@@ -74,76 +55,13 @@
- -
- - - - - - {{(hits || 0) | number:0}} - -
-
- -
-
- -

No results found

- -

- Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas: -

- -
-

Shard Failures

-

The following shard failures ocurred:

-
    -
  • Index: {{failure.index}} Shard: {{failure.shard}} Reason: {{failure.reason}}
  • -
- Show More - Show Less -
- -
-

-

Expand your time range

-

I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Click the button below to open the time picker. For future reference you can open the time picker by clicking the time picker in the top right corner of your screen. -

-
- -

Refine your query

-

- The search bar at the top uses Elasticsearch's support for Lucene Query String syntax. Let's say we're searching web server logs that have been parsed into a few fields. -

- -

-

Examples:

- Find requests that contain the number 200, in any field: -
200
- - Or we can search in a specific field. Find 200 in the status field: -
status:200
- - Find all status codes between 400-499: -
status:[400 TO 499]
- - Find status codes 400-499 with the extension php: -
status:[400 TO 499] AND extension:PHP
- - Or HTML -
status:[400 TO 499] AND (extension:php OR extension:html)
-

- -
-
+

Searching

-
{{fetchStatus.complete}}/{{fetchStatus.total}}
@@ -176,7 +94,13 @@

Searching

- + +
diff --git a/src/plugins/kibana/public/discover/index.js b/src/plugins/kibana/public/discover/index.js index 1a34560164418..2888306ae454f 100644 --- a/src/plugins/kibana/public/discover/index.js +++ b/src/plugins/kibana/public/discover/index.js @@ -1,14 +1,14 @@ -define(function (require, module, exports) { - require('plugins/kibana/discover/saved_searches/saved_searches'); - require('plugins/kibana/discover/directives/timechart'); - require('ui/collapsible_sidebar'); - require('plugins/kibana/discover/components/field_chooser/field_chooser'); - require('plugins/kibana/discover/controllers/discover'); - require('plugins/kibana/discover/styles/main.less'); +import 'plugins/kibana/discover/saved_searches/saved_searches'; +import 'plugins/kibana/discover/directives/no_results'; +import 'plugins/kibana/discover/directives/timechart'; +import 'ui/navbar_extensions'; +import 'ui/collapsible_sidebar'; +import 'plugins/kibana/discover/components/field_chooser/field_chooser'; +import 'plugins/kibana/discover/controllers/discover'; +import 'plugins/kibana/discover/styles/main.less'; +import 'ui/doc_table/components/table_row'; +import savedObjectRegistry from 'ui/saved_objects/saved_object_registry'; - // preload - require('ui/doc_table/components/table_row'); +// preload - require('ui/saved_objects/saved_object_registry').register(require('plugins/kibana/discover/saved_searches/saved_search_register')); - -}); +savedObjectRegistry.register(require('plugins/kibana/discover/saved_searches/saved_search_register')); diff --git a/src/plugins/kibana/public/discover/partials/no_results.html b/src/plugins/kibana/public/discover/partials/no_results.html new file mode 100644 index 0000000000000..eb3b434baebb4 --- /dev/null +++ b/src/plugins/kibana/public/discover/partials/no_results.html @@ -0,0 +1,51 @@ +
+
+ +

No results found

+ +

+ Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas: +

+ +
+

Shard Failures

+

The following shard failures ocurred:

+
    +
  • Index: {{failure.index}} Shard: {{failure.shard}} Reason: {{failure.reason}}
  • +
+ Show More + Show Less +
+ +
+

+

Expand your time range

+

I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Click the button below to open the time picker. For future reference you can open the time picker by clicking on the time picker button in the top right corner of your screen. +

+
+ +

Refine your query

+

+ The search bar at the top uses Elasticsearch's support for Lucene Query String syntax. Let's say we're searching web server logs that have been parsed into a few fields. +

+ +

+

Examples:

+ Find requests that contain the number 200, in any field: +
200
+ + Or we can search in a specific field. Find 200 in the status field: +
status:200
+ + Find all status codes between 400-499: +
status:[400 TO 499]
+ + Find status codes 400-499 with the extension php: +
status:[400 TO 499] AND extension:PHP
+ + Or HTML +
status:[400 TO 499] AND (extension:php OR extension:html)
+

+ +
+
diff --git a/src/plugins/kibana/public/discover/partials/save_search.html b/src/plugins/kibana/public/discover/partials/save_search.html index ff4aeaa2de933..6ab2178e1018a 100644 --- a/src/plugins/kibana/public/discover/partials/save_search.html +++ b/src/plugins/kibana/public/discover/partials/save_search.html @@ -7,7 +7,7 @@
-
diff --git a/src/plugins/kibana/public/discover/partials/share_search.html b/src/plugins/kibana/public/discover/partials/share_search.html new file mode 100644 index 0000000000000..69fee7ad756d0 --- /dev/null +++ b/src/plugins/kibana/public/discover/partials/share_search.html @@ -0,0 +1,5 @@ + + diff --git a/src/plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/plugins/kibana/public/discover/saved_searches/_saved_search.js index 0f678b49770cb..7ad9d46bfb54a 100644 --- a/src/plugins/kibana/public/discover/saved_searches/_saved_search.js +++ b/src/plugins/kibana/public/discover/saved_searches/_saved_search.js @@ -1,46 +1,45 @@ -define(function (require) { - var _ = require('lodash'); - - require('ui/notify'); - - var module = require('ui/modules').get('discover/saved_searches', [ - 'kibana/notify', - 'kibana/courier' - ]); - - module.factory('SavedSearch', function (courier) { - _.class(SavedSearch).inherits(courier.SavedObject); - function SavedSearch(id) { - courier.SavedObject.call(this, { - type: SavedSearch.type, - mapping: SavedSearch.mapping, - searchSource: SavedSearch.searchSource, - - id: id, - defaults: { - title: 'New Saved Search', - description: '', - columns: [], - hits: 0, - sort: [], - version: 1 - } - }); - } - - SavedSearch.type = 'search'; - - SavedSearch.mapping = { - title: 'string', - description: 'string', - hits: 'integer', - columns: 'string', - sort: 'string', - version: 'integer' - }; - - SavedSearch.searchSource = true; - - return SavedSearch; - }); +import _ from 'lodash'; +import 'ui/notify'; +import uiModules from 'ui/modules'; + + +const module = uiModules.get('discover/saved_searches', [ + 'kibana/notify', + 'kibana/courier' +]); + +module.factory('SavedSearch', function (courier) { + _.class(SavedSearch).inherits(courier.SavedObject); + function SavedSearch(id) { + courier.SavedObject.call(this, { + type: SavedSearch.type, + mapping: SavedSearch.mapping, + searchSource: SavedSearch.searchSource, + + id: id, + defaults: { + title: 'New Saved Search', + description: '', + columns: [], + hits: 0, + sort: [], + version: 1 + } + }); + } + + SavedSearch.type = 'search'; + + SavedSearch.mapping = { + title: 'string', + description: 'string', + hits: 'integer', + columns: 'string', + sort: 'string', + version: 'integer' + }; + + SavedSearch.searchSource = true; + + return SavedSearch; }); diff --git a/src/plugins/kibana/public/discover/saved_searches/saved_search_register.js b/src/plugins/kibana/public/discover/saved_searches/saved_search_register.js index 82a6f38df3ce4..9ca6c654e1738 100644 --- a/src/plugins/kibana/public/discover/saved_searches/saved_search_register.js +++ b/src/plugins/kibana/public/discover/saved_searches/saved_search_register.js @@ -1,5 +1,3 @@ -define(function (require) { - return function savedSearchObjectFn(savedSearches) { - return savedSearches; - }; -}); +export default function savedSearchObjectFn(savedSearches) { + return savedSearches; +}; diff --git a/src/plugins/kibana/public/discover/saved_searches/saved_searches.js b/src/plugins/kibana/public/discover/saved_searches/saved_searches.js index cf3c1895eb1e8..0244613baeccb 100644 --- a/src/plugins/kibana/public/discover/saved_searches/saved_searches.js +++ b/src/plugins/kibana/public/discover/saved_searches/saved_searches.js @@ -1,85 +1,98 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import Scanner from 'ui/utils/scanner'; +import 'plugins/kibana/discover/saved_searches/_saved_search'; +import 'ui/notify'; +import uiModules from 'ui/modules'; - require('plugins/kibana/discover/saved_searches/_saved_search'); - require('ui/notify'); - var module = require('ui/modules').get('discover/saved_searches', [ - 'kibana/notify' - ]); +const module = uiModules.get('discover/saved_searches', [ + 'kibana/notify' +]); - // Register this service with the saved object registry so it can be - // edited by the object editor. - require('plugins/kibana/settings/saved_object_registry').register({ - service: 'savedSearches', - title: 'searches' +// Register this service with the saved object registry so it can be +// edited by the object editor. +require('plugins/kibana/settings/saved_object_registry').register({ + service: 'savedSearches', + title: 'searches' +}); + +module.service('savedSearches', function (Promise, config, kbnIndex, es, createNotifier, SavedSearch, kbnUrl) { + const scanner = new Scanner(es, { + index: kbnIndex, + type: 'search' + }); + + const notify = createNotifier({ + location: 'Saved Searches' }); - module.service('savedSearches', function (Promise, config, kbnIndex, es, createNotifier, SavedSearch, kbnUrl) { + this.type = SavedSearch.type; + this.Class = SavedSearch; + + this.loaderProperties = { + name: 'searches', + noun: 'Saved Search', + nouns: 'saved searches' + }; + + + this.scanAll = function (queryString, pageSize = 1000) { + return scanner.scanAndMap(queryString, { + pageSize, + docCount: Infinity + }, (hit) => this.mapHits(hit)); + }; - var notify = createNotifier({ - location: 'Saved Searches' + this.get = function (id) { + return (new SavedSearch(id)).init(); + }; + + this.urlFor = function (id) { + return kbnUrl.eval('#/discover/{{id}}', {id: id}); + }; + + this.delete = function (ids) { + ids = !_.isArray(ids) ? [ids] : ids; + return Promise.map(ids, function (id) { + return (new SavedSearch(id)).delete(); }); + }; + + this.mapHits = function (hit) { + const source = hit._source; + source.id = hit._id; + source.url = this.urlFor(hit._id); + return source; + }; - this.type = SavedSearch.type; - this.Class = SavedSearch; - - this.loaderProperties = { - name: 'searches', - noun: 'Saved Search', - nouns: 'saved searches' - }; - - this.get = function (id) { - return (new SavedSearch(id)).init(); - }; - - this.urlFor = function (id) { - return kbnUrl.eval('#/discover/{{id}}', {id: id}); - }; - - this.delete = function (ids) { - ids = !_.isArray(ids) ? [ids] : ids; - return Promise.map(ids, function (id) { - return (new SavedSearch(id)).delete(); - }); - }; - - this.find = function (searchString, size = 100) { - var self = this; - var body; - if (searchString) { - body = { - query: { - simple_query_string: { - query: searchString + '*', - fields: ['title^3', 'description'], - default_operator: 'AND' - } + this.find = function (searchString, size = 100) { + let body; + if (searchString) { + body = { + query: { + simple_query_string: { + query: searchString + '*', + fields: ['title^3', 'description'], + default_operator: 'AND' } - }; - } else { - body = { query: {match_all: {}}}; - } - - return es.search({ - index: kbnIndex, - type: 'search', - body: body, - size: size - }) - .then(function (resp) { - return { - total: resp.hits.total, - hits: resp.hits.hits.map(function (hit) { - var source = hit._source; - source.id = hit._id; - source.url = self.urlFor(hit._id); - return source; - }) - }; - }); - }; - }); + } + }; + } else { + body = { query: {match_all: {}}}; + } + + return es.search({ + index: kbnIndex, + type: 'search', + body: body, + size: size + }) + .then((resp) => { + return { + total: resp.hits.total, + hits: resp.hits.hits.map((hit) => this.mapHits(hit)) + }; + }); + }; }); diff --git a/src/plugins/kibana/public/discover/styles/main.less b/src/plugins/kibana/public/discover/styles/main.less index 13b53fc815b18..f9c6a466e61b6 100644 --- a/src/plugins/kibana/public/discover/styles/main.less +++ b/src/plugins/kibana/public/discover/styles/main.less @@ -29,6 +29,10 @@ height: 200px; max-height: 600px; + .loading { + opacity: 1 !important; + } + &.only-spy { height: auto; @@ -71,12 +75,10 @@ } &-info { - background-color: @discover-info-bg; - float: right; - padding: 5px 10px; + line-height: 30px; + padding: 0px 10px; border-bottom-left-radius: @border-radius-base; - text-align: right; &-title { font-weight: bold; @@ -245,6 +247,10 @@ disc-field-chooser { } } + .sidebar-item.active .sidebar-item-title { + background-color: @sidebar-active-bg; + color: @sidebar-active-color; + } .sidebar-item-title { position: relative; } diff --git a/src/plugins/kibana/public/doc/__tests__/doc.js b/src/plugins/kibana/public/doc/__tests__/doc.js index 88823cf0fc7dd..5efbe5a49f980 100644 --- a/src/plugins/kibana/public/doc/__tests__/doc.js +++ b/src/plugins/kibana/public/doc/__tests__/doc.js @@ -1,13 +1,14 @@ // Load the kibana app dependencies. -var ngMock = require('ngMock'); -var expect = require('expect.js'); -require('plugins/kibana/doc/index'); +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import 'plugins/kibana/doc/index'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -var $scope; -var createController; -var timefilter; +let $scope; +let createController; +let timefilter; -var init = function (index, type, id) { +const init = function (index, type, id) { ngMock.module('kibana'); @@ -16,7 +17,7 @@ var init = function (index, type, id) { $provide.service('$route', function (Private) { this.current = { locals: { - indexPattern: Private(require('fixtures/stubbed_logstash_index_pattern')) + indexPattern: Private(FixturesStubbedLogstashIndexPatternProvider) }, params: { index: index || 'myIndex', @@ -28,7 +29,7 @@ var init = function (index, type, id) { $provide.service('es', function (Private, $q) { this.search = function (config) { - var deferred = $q.defer(); + const deferred = $q.defer(); switch (config.index) { case 'goodSearch': diff --git a/src/plugins/kibana/public/doc/controllers/doc.js b/src/plugins/kibana/public/doc/controllers/doc.js index fa109900833e5..d42ca2ca876e7 100644 --- a/src/plugins/kibana/public/doc/controllers/doc.js +++ b/src/plugins/kibana/public/doc/controllers/doc.js @@ -1,75 +1,75 @@ -define(function (require) { - var _ = require('lodash'); - var angular = require('angular'); +import _ from 'lodash'; +import angular from 'angular'; +import 'ui/notify'; +import 'ui/courier'; +import 'ui/doc_viewer'; +import 'ui/index_patterns'; +import html from 'plugins/kibana/doc/index.html'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; - require('ui/notify'); - require('ui/courier'); - require('ui/doc_viewer'); - require('ui/index_patterns'); - var app = require('ui/modules').get('apps/doc', [ - 'kibana/notify', - 'kibana/courier', - 'kibana/index_patterns' - ]); +const app = uiModules.get('apps/doc', [ + 'kibana/notify', + 'kibana/courier', + 'kibana/index_patterns' +]); - var html = require('plugins/kibana/doc/index.html'); - var resolveIndexPattern = { - indexPattern: function (courier, savedSearches, $route) { - return courier.indexPatterns.get($route.current.params.indexPattern); - } - }; +const resolveIndexPattern = { + indexPattern: function (courier, savedSearches, $route) { + return courier.indexPatterns.get($route.current.params.indexPattern); + } +}; - require('ui/routes') - .when('/doc/:indexPattern/:index/:type/:id', { - template: html, - resolve: resolveIndexPattern - }) - .when('/doc/:indexPattern/:index/:type', { - template: html, - resolve: resolveIndexPattern - }); +uiRoutes +.when('/doc/:indexPattern/:index/:type/:id', { + template: html, + resolve: resolveIndexPattern +}) +.when('/doc/:indexPattern/:index/:type', { + template: html, + resolve: resolveIndexPattern +}); - app.controller('doc', function ($scope, $route, es, timefilter) { +app.controller('doc', function ($scope, $route, es, timefilter) { - timefilter.enabled = false; + timefilter.enabled = false; - // Pretty much only need this for formatting, not actually using it for fetching anything. - $scope.indexPattern = $route.current.locals.indexPattern; + // Pretty much only need this for formatting, not actually using it for fetching anything. + $scope.indexPattern = $route.current.locals.indexPattern; - var computedFields = $scope.indexPattern.getComputedFields(); + const computedFields = $scope.indexPattern.getComputedFields(); - es.search({ - index: $route.current.params.index, - body: { - query: { - ids: { - type: $route.current.params.type, - values: [$route.current.params.id] - } - }, - fields: computedFields.fields, - script_fields: computedFields.scriptFields, - fielddata_fields: computedFields.fielddataFields - } - }).then(function (resp) { - if (resp.hits) { - if (resp.hits.total < 1) { - $scope.status = 'notFound'; - } else { - $scope.status = 'found'; - $scope.hit = resp.hits.hits[0]; + es.search({ + index: $route.current.params.index, + body: { + query: { + ids: { + type: $route.current.params.type, + values: [$route.current.params.id] } - } - }).catch(function (err) { - if (err.status === 404) { + }, + fields: computedFields.fields, + script_fields: computedFields.scriptFields, + fielddata_fields: computedFields.fielddataFields + } + }).then(function (resp) { + if (resp.hits) { + if (resp.hits.total < 1) { $scope.status = 'notFound'; } else { - $scope.status = 'error'; - $scope.resp = err; + $scope.status = 'found'; + $scope.hit = resp.hits.hits[0]; } - }); - + } + }).catch(function (err) { + if (err.status === 404) { + $scope.status = 'notFound'; + } else { + $scope.status = 'error'; + $scope.resp = err; + } }); + }); diff --git a/src/plugins/kibana/public/doc/index.js b/src/plugins/kibana/public/doc/index.js index 23d3c2ee40fb6..7f57d3e414b26 100644 --- a/src/plugins/kibana/public/doc/index.js +++ b/src/plugins/kibana/public/doc/index.js @@ -1,3 +1 @@ -define(function (require, module, exports) { - require('plugins/kibana/doc/controllers/doc'); -}); +import 'plugins/kibana/doc/controllers/doc'; diff --git a/src/plugins/kibana/public/kibana.js b/src/plugins/kibana/public/kibana.js index 698eb43faa26d..764f19dec9f77 100644 --- a/src/plugins/kibana/public/kibana.js +++ b/src/plugins/kibana/public/kibana.js @@ -1,16 +1,25 @@ -require('plugins/kibana/discover/index'); -require('plugins/kibana/visualize/index'); -require('plugins/kibana/dashboard/index'); -require('plugins/kibana/settings/index'); -require('plugins/kibana/doc/index'); +// autoloading -var moment = require('moment-timezone'); +// preloading (for faster webpack builds) +import moment from 'moment-timezone'; +import chrome from 'ui/chrome'; +import routes from 'ui/routes'; +import modules from 'ui/modules'; -var chrome = require('ui/chrome'); -var routes = require('ui/routes'); -var modules = require('ui/modules'); +import kibanaLogoUrl from 'ui/images/kibana.svg'; +import 'ui/autoload/all'; +import 'plugins/kibana/discover/index'; +import 'plugins/kibana/visualize/index'; +import 'plugins/kibana/dashboard/index'; +import 'plugins/kibana/settings/index'; +import 'plugins/kibana/doc'; +import 'ui/vislib'; +import 'ui/agg_response'; +import 'ui/agg_types'; +import 'ui/timepicker'; +import 'leaflet'; -var kibanaLogoUrl = require('ui/images/kibana.svg'); +routes.enable(); routes .otherwise({ @@ -18,47 +27,20 @@ routes }); chrome -.setBrand({ - 'logo': 'url(' + kibanaLogoUrl + ') left no-repeat', - 'smallLogo': 'url(' + kibanaLogoUrl + ') left no-repeat' -}) -.setNavBackground('#222222') .setTabDefaults({ resetWhenActive: true, - lastUrlStore: window.sessionStore, + lastUrlStore: window.sessionStorage, activeIndicatorColor: '#656a76' }) -.setTabs([ - { - id: 'discover', - title: 'Discover' - }, - { - id: 'visualize', - title: 'Visualize', - activeIndicatorColor: function () { - return (String(this.lastUrl).indexOf('/visualize/step/') === 0) ? 'white' : '#656a76'; - } - }, - { - id: 'dashboard', - title: 'Dashboard' - }, - { - id: 'settings', - title: 'Settings' - } -]) -.setRootController('kibana', function ($scope, $rootScope, courier, config) { - function setDefaultTimezone() { - moment.tz.setDefault(config.get('dateFormat:tz')); - } - +.setRootController('kibana', function ($scope, courier, config) { // wait for the application to finish loading $scope.$on('application.load', function () { courier.start(); }); - $scope.$on('init:config', setDefaultTimezone); - $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); + config.watch('dateFormat:tz', setDefaultTimezone, $scope); + + function setDefaultTimezone(tz) { + moment.tz.setDefault(tz); + } }); diff --git a/src/plugins/kibana/public/settings/app.html b/src/plugins/kibana/public/settings/app.html index 0d86dff90fd96..f78a64d809790 100644 --- a/src/plugins/kibana/public/settings/app.html +++ b/src/plugins/kibana/public/settings/app.html @@ -1,9 +1,10 @@
-
diff --git a/src/plugins/kibana/public/settings/sections/about/index.js b/src/plugins/kibana/public/settings/sections/about/index.js index fec9ad2da3165..3040b7a89cb98 100644 --- a/src/plugins/kibana/public/settings/sections/about/index.js +++ b/src/plugins/kibana/public/settings/sections/about/index.js @@ -1,22 +1,25 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import registry from 'ui/registry/settings_sections'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import indexTemplate from 'plugins/kibana/settings/sections/about/index.html'; - require('ui/routes') - .when('/settings/about', { - template: require('plugins/kibana/settings/sections/about/index.html') - }); - - require('ui/modules').get('apps/settings') - .controller('settingsAbout', function ($scope, kbnVersion, buildNum, buildSha) { - $scope.kbnVersion = kbnVersion; - $scope.buildNum = buildNum; - $scope.buildSha = buildSha; - }); +uiRoutes +.when('/settings/about', { + template: indexTemplate +}); - return { - order: Infinity, - name: 'about', - display: 'About', - url: '#/settings/about' - }; +uiModules.get('apps/settings') +.controller('settingsAbout', function ($scope, kbnVersion, buildNum, buildSha, serverName) { + $scope.kbnVersion = kbnVersion; + $scope.buildNum = buildNum; + $scope.buildSha = buildSha; + $scope.serverName = serverName; }); + +registry.register(_.constant({ + order: 1001, + name: 'about', + display: 'About', + url: '#/settings/about' +})); diff --git a/src/plugins/kibana/public/settings/sections/advanced/advanced_row.html b/src/plugins/kibana/public/settings/sections/advanced/advanced_row.html index 51d419b166715..4b57f33041143 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/advanced_row.html +++ b/src/plugins/kibana/public/settings/sections/advanced/advanced_row.html @@ -1,4 +1,4 @@ - + {{conf.name}} @@ -16,7 +16,7 @@
@@ -59,12 +59,13 @@ name="conf.name" ng-model="conf.unsavedValue" ng-options="option as option for option in conf.options" - class="form-control"> + class="form-control" + data-test-subj="selectInput">
- + {{conf.value || conf.defVal}} {{(conf.value || conf.defVal).join(', ')}} {{conf.value === undefined ? conf.defVal : conf.value}} @@ -73,41 +74,45 @@ diff --git a/src/plugins/kibana/public/settings/sections/advanced/advanced_row.js b/src/plugins/kibana/public/settings/sections/advanced/advanced_row.js index 7371827e073fb..5b82fdc2907ae 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/advanced_row.js +++ b/src/plugins/kibana/public/settings/sections/advanced/advanced_row.js @@ -1,71 +1,70 @@ -define(function (require) { - var _ = require('lodash'); - require('ui/elastic_textarea'); +import _ from 'lodash'; +import 'ui/elastic_textarea'; +import uiModules from 'ui/modules'; +import advancedRowTemplate from 'plugins/kibana/settings/sections/advanced/advanced_row.html'; - require('ui/modules').get('apps/settings') - .directive('advancedRow', function (config, Notifier, Private) { - return { - restrict: 'A', - replace: true, - template: require('plugins/kibana/settings/sections/advanced/advanced_row.html'), - scope: { - conf: '=advancedRow', - configs: '=' - }, - link: function ($scope) { - var configDefaults = Private(require('ui/config/defaults')); - var notify = new Notifier(); - var keyCodes = { - ESC: 27 - }; +uiModules.get('apps/settings') +.directive('advancedRow', function (config, Notifier) { + return { + restrict: 'A', + replace: true, + template: advancedRowTemplate, + scope: { + conf: '=advancedRow', + configs: '=' + }, + link: function ($scope) { + const notify = new Notifier(); + const keyCodes = { + ESC: 27 + }; - // To allow passing form validation state back - $scope.forms = {}; + // To allow passing form validation state back + $scope.forms = {}; - // setup loading flag, run async op, then clear loading and editting flag (just in case) - var loading = function (conf, fn) { - conf.loading = true; - fn() - .finally(function () { - conf.loading = conf.editting = false; + // setup loading flag, run async op, then clear loading and editing flag (just in case) + const loading = function (conf, fn) { + conf.loading = true; + fn() + .then(function () { + conf.loading = conf.editing = false; }) .catch(notify.fatal); - }; + }; - $scope.maybeCancel = function ($event, conf) { - if ($event.keyCode === keyCodes.ESC) { - $scope.cancelEdit(conf); - } - }; + $scope.maybeCancel = function ($event, conf) { + if ($event.keyCode === keyCodes.ESC) { + $scope.cancelEdit(conf); + } + }; - $scope.edit = function (conf) { - conf.unsavedValue = conf.value == null ? conf.defVal : conf.value; - $scope.configs.forEach(function (c) { - c.editting = (c === conf); - }); - }; + $scope.edit = function (conf) { + conf.unsavedValue = conf.value == null ? conf.defVal : conf.value; + $scope.configs.forEach(function (c) { + c.editing = (c === conf); + }); + }; - $scope.save = function (conf) { - loading(conf, function () { - if (conf.unsavedValue === conf.defVal) { - return config.clear(conf.name); - } + $scope.save = function (conf) { + loading(conf, function () { + if (conf.unsavedValue === conf.defVal) { + return config.remove(conf.name); + } - return config.set(conf.name, conf.unsavedValue); - }); - }; + return config.set(conf.name, conf.unsavedValue); + }); + }; - $scope.cancelEdit = function (conf) { - conf.editting = false; - }; + $scope.cancelEdit = function (conf) { + conf.editing = false; + }; - $scope.clear = function (conf) { - return loading(conf, function () { - return config.clear(conf.name); - }); - }; + $scope.clear = function (conf) { + return loading(conf, function () { + return config.remove(conf.name); + }); + }; - } - }; - }); + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/advanced/index.html b/src/plugins/kibana/public/settings/sections/advanced/index.html index a2894631dc868..13f85f7c88194 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/index.html +++ b/src/plugins/kibana/public/settings/sections/advanced/index.html @@ -3,7 +3,7 @@

Caution: You can break stuff here

Be careful in here, these settings are for very advanced users only. - Tweaks you make here can break large portionsof Kibana. Some of these + Tweaks you make here can break large portions of Kibana. Some of these settings may be undocumented, unsupported or experimental. If a field has a default value, blanking the field will reset it to its default which may be unacceptable given other configuration directives. Deleting a diff --git a/src/plugins/kibana/public/settings/sections/advanced/index.js b/src/plugins/kibana/public/settings/sections/advanced/index.js index bcff939994f56..2e7cbb0e7524d 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/index.js +++ b/src/plugins/kibana/public/settings/sections/advanced/index.js @@ -1,61 +1,47 @@ -define(function (require) { - var _ = require('lodash'); - var toEditableConfig = require('plugins/kibana/settings/sections/advanced/lib/to_editable_config'); - - - require('plugins/kibana/settings/sections/advanced/advanced_row'); - - require('ui/routes') - .when('/settings/advanced', { - template: require('plugins/kibana/settings/sections/advanced/index.html') - }); - - require('ui/modules').get('apps/settings') - .directive('kbnSettingsAdvanced', function (config, Notifier, Private, $rootScope) { - return { - restrict: 'E', - link: function ($scope) { - var configDefaults = Private(require('ui/config/defaults')); - var keyCodes = { - ESC: 27 - }; - - function isTypeComplex(conf) { - return !(conf.json || conf.array || conf.bool || conf.normal); - } - - function notDefaultConfig(configName) { - return !(configName in configDefaults); - } - - function readConfigVals() { - var configVals = config._vals(); - - var customConfig = Object.keys(configVals) - .filter(notDefaultConfig) - .map(name => toEditableConfig(false, name, configVals[name])); +import _ from 'lodash'; +import registry from 'ui/registry/settings_sections'; +import toEditableConfig from 'plugins/kibana/settings/sections/advanced/lib/to_editable_config'; +import 'plugins/kibana/settings/sections/advanced/advanced_row'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import indexTemplate from 'plugins/kibana/settings/sections/advanced/index.html'; + +uiRoutes +.when('/settings/advanced', { + template: indexTemplate +}); - $scope.configs = _(configDefaults) - .map((def, name) => toEditableConfig(def, name, configVals[name])) - .reject('readonly') - .concat(customConfig) +uiModules.get('apps/settings') +.directive('kbnSettingsAdvanced', function (config, Notifier, Private, $rootScope) { + return { + restrict: 'E', + link: function ($scope) { + // react to changes of the config values + config.watchAll(changed, $scope); + + // initial config setup + changed(); + + function changed(values) { + const all = config.getAll(); + const editable = _(all) + .map((def, name) => toEditableConfig({ + def, + name, + value: def.userValue, + isCustom: config.isCustom(name) + })) .value(); - } - - // react to changes of the config values - var unhook = $rootScope.$on('change:config', readConfigVals); - $scope.$on('$destroy', unhook); - - // initial config setup - readConfigVals(); + const writable = _.reject(editable, 'readonly'); + $scope.configs = writable; } - }; - }); - - return { - order: 2, - name: 'advanced', - display: 'Advanced', - url: '#/settings/advanced' + } }; }); + +registry.register(_.constant({ + order: 2, + name: 'advanced', + display: 'Advanced', + url: '#/settings/advanced' +})); diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_editor_type.js b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_editor_type.js index 968a3cf3f44bf..2b230c3fe5a22 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_editor_type.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_editor_type.js @@ -1,6 +1,6 @@ -var getEditorType = require('plugins/kibana/settings/sections/advanced/lib/get_editor_type'); -var expect = require('expect.js'); +import getEditorType from 'plugins/kibana/settings/sections/advanced/lib/get_editor_type'; +import expect from 'expect.js'; describe('Settings', function () { describe('Advanced', function () { diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_val_type.js b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_val_type.js index 64742a3766fd6..d92162787f834 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_val_type.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/get_val_type.js @@ -1,6 +1,6 @@ -var getValType = require('plugins/kibana/settings/sections/advanced/lib/get_val_type'); -var expect = require('expect.js'); +import getValType from 'plugins/kibana/settings/sections/advanced/lib/get_val_type'; +import expect from 'expect.js'; describe('Settings', function () { describe('Advanced', function () { diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/to_editable_config.js b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/to_editable_config.js index 7a9a78d34f0ad..91690be14d9a1 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/to_editable_config.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/__tests__/to_editable_config.js @@ -1,6 +1,6 @@ -var toEditableConfig = require('plugins/kibana/settings/sections/advanced/lib/to_editable_config'); -var expect = require('expect.js'); +import toEditableConfig from 'plugins/kibana/settings/sections/advanced/lib/to_editable_config'; +import expect from 'expect.js'; describe('Settings', function () { describe('Advanced', function () { @@ -20,7 +20,7 @@ describe('Settings', function () { }); context('when given a setting definition object', function () { - var def; + let def; beforeEach(function () { def = { value: 'the original', @@ -82,5 +82,5 @@ describe('Settings', function () { }); function invoke({ def = false, name = 'woah', value = 'forreal' } = {}) { - return toEditableConfig(def, name, value); + return toEditableConfig({ def, name, value, isCustom: def === false }); } diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/get_editor_type.js b/src/plugins/kibana/public/settings/sections/advanced/lib/get_editor_type.js index 54ec1e240bbf3..a9ce2bd3b962a 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/get_editor_type.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/get_editor_type.js @@ -1,17 +1,15 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; - var NAMED_EDITORS = ['json', 'array', 'boolean', 'select']; - var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined']; +const NAMED_EDITORS = ['json', 'array', 'boolean', 'select']; +const NORMAL_EDITOR = ['number', 'string', 'null', 'undefined']; - /** - * @param {object} advanced setting configuration object - * @returns {string} the editor type to use when editing value - */ - function getEditorType(conf) { - if (_.contains(NAMED_EDITORS, conf.type)) return conf.type; - if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal'; - } +/** + * @param {object} advanced setting configuration object + * @returns {string} the editor type to use when editing value + */ +function getEditorType(conf) { + if (_.contains(NAMED_EDITORS, conf.type)) return conf.type; + if (_.contains(NORMAL_EDITOR, conf.type)) return 'normal'; +} - return getEditorType; -}); +export default getEditorType; diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/get_val_type.js b/src/plugins/kibana/public/settings/sections/advanced/lib/get_val_type.js index 08ea1dd483deb..7b9f65c9a4925 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/get_val_type.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/get_val_type.js @@ -1,22 +1,20 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; - /** - * @param {object} advanced setting definition object - * @param {?} current value of the setting - * @returns {string} the type to use for determining the display and editor - */ - function getValType(def, value) { - if (def.type) { - return def.type; - } - - if (_.isArray(value) || _.isArray(def.value)) { - return 'array'; - } +/** + * @param {object} advanced setting definition object + * @param {?} current value of the setting + * @returns {string} the type to use for determining the display and editor + */ +function getValType(def, value) { + if (def.type) { + return def.type; + } - return (def.value != null ? typeof def.value : typeof value); + if (_.isArray(value) || _.isArray(def.value)) { + return 'array'; } - return getValType; -}); + return (def.value != null ? typeof def.value : typeof value); +} + +export default getValType; diff --git a/src/plugins/kibana/public/settings/sections/advanced/lib/to_editable_config.js b/src/plugins/kibana/public/settings/sections/advanced/lib/to_editable_config.js index a147cb61c31e4..d4f4b6d8efcae 100644 --- a/src/plugins/kibana/public/settings/sections/advanced/lib/to_editable_config.js +++ b/src/plugins/kibana/public/settings/sections/advanced/lib/to_editable_config.js @@ -1,39 +1,37 @@ -define(function (require) { - var _ = require('lodash'); - var getValType = require('./get_val_type'); - var getEditorType = require('./get_editor_type'); +import _ from 'lodash'; +import getValType from './get_val_type'; +import getEditorType from './get_editor_type'; - /** - * @param {object} advanced setting definition object - * @param {object} name of setting - * @param {object} current value of setting - * @returns {object} the editable config object - */ - function toEditableConfig(def, name, value) { - var isCustom = !def; - if (isCustom) def = {}; - - var conf = { - name, - value, - isCustom, - readonly: !!def.readonly, - defVal: def.value, - type: getValType(def, value), - description: def.description, - options: def.options - }; +/** + * @param {object} advanced setting definition object + * @param {object} name of setting + * @param {object} current value of setting + * @returns {object} the editable config object + */ +function toEditableConfig({ def, name, value, isCustom }) { + if (!def) { + def = {}; + } + const conf = { + name, + value, + isCustom, + readonly: !!def.readonly, + defVal: def.value, + type: getValType(def, value), + description: def.description, + options: def.options + }; - var editor = getEditorType(conf); - conf.json = editor === 'json'; - conf.select = editor === 'select'; - conf.bool = editor === 'boolean'; - conf.array = editor === 'array'; - conf.normal = editor === 'normal'; - conf.tooComplex = !editor; + const editor = getEditorType(conf); + conf.json = editor === 'json'; + conf.select = editor === 'select'; + conf.bool = editor === 'boolean'; + conf.array = editor === 'array'; + conf.normal = editor === 'normal'; + conf.tooComplex = !editor; - return conf; - } + return conf; +} - return toEditableConfig; -}); +export default toEditableConfig; diff --git a/src/plugins/kibana/public/settings/sections/index.js b/src/plugins/kibana/public/settings/sections/index.js deleted file mode 100644 index c2be1402c526c..0000000000000 --- a/src/plugins/kibana/public/settings/sections/index.js +++ /dev/null @@ -1,11 +0,0 @@ -define(function (require) { - // each of these private modules returns an object defining that section, their properties - // are used to create the nav bar - return [ - require('plugins/kibana/settings/sections/indices/index'), - require('plugins/kibana/settings/sections/advanced/index'), - require('plugins/kibana/settings/sections/objects/index'), - require('plugins/kibana/settings/sections/status/index'), - require('plugins/kibana/settings/sections/about/index') - ]; -}); diff --git a/src/plugins/kibana/public/settings/sections/indices/_create.html b/src/plugins/kibana/public/settings/sections/indices/_create.html index 9f18d319050ac..4bf6c76fec6c5 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_create.html +++ b/src/plugins/kibana/public/settings/sections/indices/_create.html @@ -73,6 +73,36 @@

Time-interval based index patterns are deprecated!

+
+ + +
+ This index pattern will be queried directly rather than being + expanded into more performant searches against individual indices. + + Elasticsearch will receive a query against {{index.name}} + and will have to search through all matching indices regardless + of whether they have data that matches the current time range. +
+ +

+ By default, searches against any time-based index pattern that + contains a wildcard will automatically be expanded to query only + the indices that contain data within the currently selected time + range. +

+ +

+ Searching against the index pattern logstash-* will + actually query elasticsearch for the specific matching indices + (e.g. logstash-2015.12.21) that fall within the current + time range. +

+
+
{{err}} diff --git a/src/plugins/kibana/public/settings/sections/indices/_create.js b/src/plugins/kibana/public/settings/sections/indices/_create.js index e701e310f3341..78069125040df 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_create.js +++ b/src/plugins/kibana/public/settings/sections/indices/_create.js @@ -1,283 +1,296 @@ -define(function (require) { - var _ = require('lodash'); - var moment = require('moment'); - var { IndexPatternMissingIndices } = require('ui/errors'); - - require('ui/directives/validate_index_name'); - require('ui/directives/auto_select_if_only_one'); - - require('ui/routes') - .when('/settings/indices/', { - template: require('plugins/kibana/settings/sections/indices/_create.html') - }); - - require('ui/modules').get('apps/settings') - .controller('settingsIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise) { - var notify = new Notifier(); - var refreshKibanaIndex = Private(require('plugins/kibana/settings/sections/indices/_refresh_kibana_index')); - var intervals = indexPatterns.intervals; - var samplePromise; - - // this and child scopes will write pattern vars here - var index = $scope.index = { - name: 'logstash-*', - - isTimeBased: true, - nameIsPattern: false, - sampleCount: 5, - nameIntervalOptions: intervals, - - fetchFieldsError: 'Loading' - }; - - index.nameInterval = _.find(index.nameIntervalOptions, { name: 'daily' }); - index.timeField = null; - - $scope.refreshFieldList = function () { - fetchFieldList().then(updateFieldList); - }; - - $scope.createIndexPattern = function () { - // get an empty indexPattern to start - indexPatterns.get() - .then(function (indexPattern) { - // set both the id and title to the index index - indexPattern.id = indexPattern.title = index.name; - if (index.isTimeBased) { - indexPattern.timeFieldName = index.timeField.name; - if (index.nameIsPattern) { - indexPattern.intervalName = index.nameInterval.name; - } - } - - // fetch the fields - return indexPattern.create() - .then(function (id) { - if (id) { - refreshKibanaIndex().then(function () { - if (!config.get('defaultIndex')) { - config.set('defaultIndex', indexPattern.id); - } - indexPatterns.cache.clear(indexPattern.id); - kbnUrl.change('/settings/indices/' + indexPattern.id); - }); - } - }); +import _ from 'lodash'; +import moment from 'moment'; +import { IndexPatternMissingIndices } from 'ui/errors'; +import 'ui/directives/validate_index_name'; +import 'ui/directives/auto_select_if_only_one'; +import PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider from 'plugins/kibana/settings/sections/indices/_refresh_kibana_index'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import createTemplate from 'plugins/kibana/settings/sections/indices/_create.html'; + + +uiRoutes +.when('/settings/indices/', { + template: createTemplate +}); - // refreshFields calls save() after a successfull fetch, no need to save again - // .then(function () { indexPattern.save(); }) - }) - .catch(function (err) { - if (err instanceof IndexPatternMissingIndices) { - notify.error('Could not locate any indices matching that pattern. Please add the index to Elasticsearch'); +uiModules.get('apps/settings') +.controller('settingsIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise) { + const notify = new Notifier(); + const refreshKibanaIndex = Private(PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider); + const intervals = indexPatterns.intervals; + let samplePromise; + + // this and child scopes will write pattern vars here + const index = $scope.index = { + name: 'logstash-*', + + isTimeBased: true, + nameIsPattern: false, + notExpandable: false, + sampleCount: 5, + nameIntervalOptions: intervals, + + fetchFieldsError: 'Loading' + }; + + index.nameInterval = _.find(index.nameIntervalOptions, { name: 'daily' }); + index.timeField = null; + + $scope.canExpandIndices = function () { + // to maximize performance in the digest cycle, move from the least + // expensive operation to most + return index.isTimeBased && !index.nameIsPattern && _.includes(index.name, '*'); + }; + + $scope.refreshFieldList = function () { + fetchFieldList().then(updateFieldList); + }; + + $scope.createIndexPattern = function () { + // get an empty indexPattern to start + indexPatterns.get() + .then(function (indexPattern) { + // set both the id and title to the index index + indexPattern.id = indexPattern.title = index.name; + if (index.isTimeBased) { + indexPattern.timeFieldName = index.timeField.name; + if (index.nameIsPattern) { + indexPattern.intervalName = index.nameInterval.name; } - else notify.fatal(err); - }); - }; - - - $scope.$watchMulti([ - 'index.isTimeBased', - 'index.nameIsPattern', - 'index.nameInterval.name' - ], function (newVal, oldVal) { - var isTimeBased = newVal[0]; - var nameIsPattern = newVal[1]; - var newDefault = getPatternDefault(newVal[2]); - var oldDefault = getPatternDefault(oldVal[2]); - - if (index.name === oldDefault) { - index.name = newDefault; - } - - if (!isTimeBased) { - index.nameIsPattern = false; } - if (!nameIsPattern) { - delete index.nameInterval; - delete index.timeField; - } else { - index.nameInterval = index.nameInterval || intervals.byName.days; - index.name = index.name || getPatternDefault(index.nameInterval); + if (index.notExpandable && $scope.canExpandIndices()) { + indexPattern.notExpandable = true; } - }); - - $scope.moreSamples = function (andUpdate) { - index.sampleCount += 5; - if (andUpdate) updateSamples(); - }; - $scope.$watchMulti([ - 'index.name', - 'index.nameInterval' - ], function (newVal, oldVal) { - var lastPromise; - resetIndex(); - samplePromise = lastPromise = updateSamples() - .then(function () { - promiseMatch(lastPromise, function () { - index.samples = null; - index.patternErrors = []; - }); - }) - .catch(function (errors) { - promiseMatch(lastPromise, function () { - index.existing = null; - index.patternErrors = errors; - }); - }) - .finally(function () { - // prevent running when no change happened (ie, first watcher call) - if (!_.isEqual(newVal, oldVal)) { - fetchFieldList().then(function (results) { - if (lastPromise === samplePromise) { - updateFieldList(results); - samplePromise = null; + // fetch the fields + return indexPattern.create() + .then(function (id) { + if (id) { + refreshKibanaIndex().then(function () { + if (!config.get('defaultIndex')) { + config.set('defaultIndex', indexPattern.id); } + indexPatterns.cache.clear(indexPattern.id); + kbnUrl.change('/settings/indices/' + indexPattern.id); }); } }); - }); - - $scope.$watchMulti([ - 'index.isTimeBased', - 'index.sampleCount' - ], $scope.refreshFieldList); - function updateSamples() { - var patternErrors = []; - - if (!index.nameInterval || !index.name) { - return Promise.resolve(); + // refreshFields calls save() after a successfull fetch, no need to save again + // .then(function () { indexPattern.save(); }) + }) + .catch(function (err) { + if (err instanceof IndexPatternMissingIndices) { + notify.error('Could not locate any indices matching that pattern. Please add the index to Elasticsearch'); } + else notify.fatal(err); + }); + }; - var pattern = mockIndexPattern(index); - - return indexPatterns.mapper.getIndicesForIndexPattern(pattern) - .catch(function (err) { - if (err instanceof IndexPatternMissingIndices) return; - notify.error(err); - }) - .then(function (existing) { - var all = _.get(existing, 'all', []); - var matches = _.get(existing, 'matches', []); - if (all.length) { - index.existing = { - class: 'success', - all: all, - matches: matches, - matchPercent: Math.round((matches.length / all.length) * 100) + '%', - failures: _.difference(all, matches) - }; - return; - } - - patternErrors.push('Pattern does not match any existing indices'); - var radius = Math.round(index.sampleCount / 2); - var samples = intervals.toIndexList(index.name, index.nameInterval, -radius, radius); - if (_.uniq(samples).length !== samples.length) { - patternErrors.push('Invalid pattern, interval does not create unique index names'); - } else { - index.samples = samples; - } + $scope.$watchMulti([ + 'index.isTimeBased', + 'index.nameIsPattern', + 'index.nameInterval.name' + ], function (newVal, oldVal) { + const isTimeBased = newVal[0]; + const nameIsPattern = newVal[1]; + const newDefault = getPatternDefault(newVal[2]); + const oldDefault = getPatternDefault(oldVal[2]); - throw patternErrors; - }); + if (index.name === oldDefault) { + index.name = newDefault; } - function fetchFieldList() { - index.dateFields = index.timeField = index.listUsed = null; - var useIndexList = index.isTimeBased && index.nameIsPattern; - var fetchFieldsError; - var dateFields; + if (!isTimeBased) { + index.nameIsPattern = false; + } - // we don't have enough info to continue - if (!index.name) { - fetchFieldsError = 'Set an index name first'; - return; - } + if (!nameIsPattern) { + delete index.nameInterval; + delete index.timeField; + } else { + index.nameInterval = index.nameInterval || intervals.byName.days; + index.name = index.name || getPatternDefault(index.nameInterval); + } + }); - if (useIndexList && !index.nameInterval) { - fetchFieldsError = 'Select the interval at which your indices are populated.'; - return; + $scope.moreSamples = function (andUpdate) { + index.sampleCount += 5; + if (andUpdate) updateSamples(); + }; + + $scope.$watchMulti([ + 'index.name', + 'index.nameInterval' + ], function (newVal, oldVal) { + let lastPromise; + resetIndex(); + samplePromise = lastPromise = updateSamples() + .then(function () { + promiseMatch(lastPromise, function () { + index.samples = null; + index.patternErrors = []; + }); + }) + .catch(function (errors) { + promiseMatch(lastPromise, function () { + index.existing = null; + index.patternErrors = errors; + }); + }) + .finally(function () { + // prevent running when no change happened (ie, first watcher call) + if (!_.isEqual(newVal, oldVal)) { + fetchFieldList().then(function (results) { + if (lastPromise === samplePromise) { + updateFieldList(results); + samplePromise = null; + } + }); } + }); + }); - return indexPatterns.mapper.clearCache(index.name) - .then(function () { - var pattern = mockIndexPattern(index); + $scope.$watchMulti([ + 'index.isTimeBased', + 'index.sampleCount' + ], $scope.refreshFieldList); - return indexPatterns.mapper.getFieldsForIndexPattern(pattern, true) - .catch(function (err) { - // TODO: we should probably display a message of some kind - if (err instanceof IndexPatternMissingIndices) { - fetchFieldsError = 'Unable to fetch mapping. Do you have indices matching the pattern?'; - return []; - } + function updateSamples() { + const patternErrors = []; - throw err; - }); - }) - .then(function (fields) { - if (fields.length > 0) { - fetchFieldsError = null; - dateFields = fields.filter(function (field) { - return field.type === 'date'; - }); - } + if (!index.nameInterval || !index.name) { + return Promise.resolve(); + } - return { - fetchFieldsError: fetchFieldsError, - dateFields: dateFields + const pattern = mockIndexPattern(index); + + return indexPatterns.mapper.getIndicesForIndexPattern(pattern) + .catch(function (err) { + if (err instanceof IndexPatternMissingIndices) return; + notify.error(err); + }) + .then(function (existing) { + const all = _.get(existing, 'all', []); + const matches = _.get(existing, 'matches', []); + if (all.length) { + index.existing = { + class: 'success', + all: all, + matches: matches, + matchPercent: Math.round((matches.length / all.length) * 100) + '%', + failures: _.difference(all, matches) }; - }, notify.fatal); - } + return; + } - function updateFieldList(results) { - index.fetchFieldsError = results.fetchFieldsError; - index.dateFields = results.dateFields; - } + patternErrors.push('Pattern does not match any existing indices'); + const radius = Math.round(index.sampleCount / 2); + const samples = intervals.toIndexList(index.name, index.nameInterval, -radius, radius); - function promiseMatch(lastPromise, cb) { - if (lastPromise === samplePromise) { - cb(); - } else if (samplePromise != null) { - // haven't hit the last promise yet, reset index params - resetIndex(); + if (_.uniq(samples).length !== samples.length) { + patternErrors.push('Invalid pattern, interval does not create unique index names'); + } else { + index.samples = samples; } + + throw patternErrors; + }); + } + + function fetchFieldList() { + index.dateFields = index.timeField = index.listUsed = null; + const useIndexList = index.isTimeBased && index.nameIsPattern; + let fetchFieldsError; + let dateFields; + + // we don't have enough info to continue + if (!index.name) { + fetchFieldsError = 'Set an index name first'; + return; } - function resetIndex() { - index.patternErrors = []; - index.samples = null; - index.existing = null; - index.fetchFieldsError = 'Loading'; + if (useIndexList && !index.nameInterval) { + fetchFieldsError = 'Select the interval at which your indices are populated.'; + return; } - function getPatternDefault(interval) { - switch (interval) { - case 'hours': - return '[logstash-]YYYY.MM.DD.HH'; - case 'days': - return '[logstash-]YYYY.MM.DD'; - case 'weeks': - return '[logstash-]GGGG.WW'; - case 'months': - return '[logstash-]YYYY.MM'; - case 'years': - return '[logstash-]YYYY'; - default: - return 'logstash-*'; + return indexPatterns.mapper.clearCache(index.name) + .then(function () { + const pattern = mockIndexPattern(index); + + return indexPatterns.mapper.getFieldsForIndexPattern(pattern, true) + .catch(function (err) { + // TODO: we should probably display a message of some kind + if (err instanceof IndexPatternMissingIndices) { + fetchFieldsError = 'Unable to fetch mapping. Do you have indices matching the pattern?'; + return []; + } + + throw err; + }); + }) + .then(function (fields) { + if (fields.length > 0) { + fetchFieldsError = null; + dateFields = fields.filter(function (field) { + return field.type === 'date'; + }); } - } - function mockIndexPattern(index) { - // trick the mapper into thinking this is an indexPattern return { - id: index.name, - intervalName: index.nameInterval + fetchFieldsError: fetchFieldsError, + dateFields: dateFields }; + }, notify.fatal); + } + + function updateFieldList(results) { + index.fetchFieldsError = results.fetchFieldsError; + index.dateFields = results.dateFields; + } + + function promiseMatch(lastPromise, cb) { + if (lastPromise === samplePromise) { + cb(); + } else if (samplePromise != null) { + // haven't hit the last promise yet, reset index params + resetIndex(); } - }); + } + + function resetIndex() { + index.patternErrors = []; + index.samples = null; + index.existing = null; + index.fetchFieldsError = 'Loading'; + } + + function getPatternDefault(interval) { + switch (interval) { + case 'hours': + return '[logstash-]YYYY.MM.DD.HH'; + case 'days': + return '[logstash-]YYYY.MM.DD'; + case 'weeks': + return '[logstash-]GGGG.WW'; + case 'months': + return '[logstash-]YYYY.MM'; + case 'years': + return '[logstash-]YYYY'; + default: + return 'logstash-*'; + } + } + + function mockIndexPattern(index) { + // trick the mapper into thinking this is an indexPattern + return { + id: index.name, + intervalName: index.nameInterval + }; + } }); diff --git a/src/plugins/kibana/public/settings/sections/indices/_date_scripts.js b/src/plugins/kibana/public/settings/sections/indices/_date_scripts.js index 791f13ba47b50..02d4788d45916 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_date_scripts.js +++ b/src/plugins/kibana/public/settings/sections/indices/_date_scripts.js @@ -1,28 +1,26 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; - return function (indexPattern) { - var dateScripts = {}; - var scripts = { - __dayOfMonth: 'dayOfMonth', - __dayOfWeek: 'dayOfWeek', - __dayOfYear: 'dayOfYear', - __hourOfDay: 'hourOfDay', - __minuteOfDay: 'minuteOfDay', - __minuteOfHour: 'minuteOfHour', - __monthOfYear: 'monthOfYear', - __weekOfYear: 'weekOfWeekyear', - __year: 'year' - }; +export default function (indexPattern) { + const dateScripts = {}; + const scripts = { + __dayOfMonth: 'dayOfMonth', + __dayOfWeek: 'dayOfWeek', + __dayOfYear: 'dayOfYear', + __hourOfDay: 'hourOfDay', + __minuteOfDay: 'minuteOfDay', + __minuteOfHour: 'minuteOfHour', + __monthOfYear: 'monthOfYear', + __weekOfYear: 'weekOfWeekyear', + __year: 'year' + }; - _.each(indexPattern.fields.byType.date, function (field) { - if (field.indexed) { - _.each(scripts, function (value, key) { - dateScripts[field.name + '.' + key] = 'doc["' + field.name + '"].date.' + value; - }); - } - }); + _.each(indexPattern.fields.byType.date, function (field) { + if (field.indexed) { + _.each(scripts, function (value, key) { + dateScripts[field.name + '.' + key] = 'doc["' + field.name + '"].date.' + value; + }); + } + }); - return dateScripts; - }; -}); + return dateScripts; +}; diff --git a/src/plugins/kibana/public/settings/sections/indices/_edit.html b/src/plugins/kibana/public/settings/sections/indices/_edit.html index fc3ca133dd156..ee0e6f65a307c 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_edit.html +++ b/src/plugins/kibana/public/settings/sections/indices/_edit.html @@ -22,6 +22,10 @@
This index uses a Time-based index pattern which repeats
+
+ This index pattern is set to be queried directly rather than being + expanded into more performant searches against individual indices. +
Mapping conflict! {{conflictFields.length > 1 ? conflictFields.length : 'A'}} field{{conflictFields.length > 1 ? 's' : ''}} {{conflictFields.length > 1 ? 'are' : 'is'}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.
diff --git a/src/plugins/kibana/public/settings/sections/indices/_edit.js b/src/plugins/kibana/public/settings/sections/indices/_edit.js index abd5ee43ea8a9..c9c6388222b83 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_edit.js +++ b/src/plugins/kibana/public/settings/sections/indices/_edit.js @@ -1,81 +1,85 @@ -define(function (require) { - var _ = require('lodash'); - require('plugins/kibana/settings/sections/indices/_indexed_fields'); - require('plugins/kibana/settings/sections/indices/_scripted_fields'); - require('plugins/kibana/settings/sections/indices/_index_header'); +import _ from 'lodash'; +import 'plugins/kibana/settings/sections/indices/_indexed_fields'; +import 'plugins/kibana/settings/sections/indices/_scripted_fields'; +import 'plugins/kibana/settings/sections/indices/_index_header'; +import PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider from 'plugins/kibana/settings/sections/indices/_refresh_kibana_index'; +import UrlProvider from 'ui/url'; +import PluginsKibanaSettingsSectionsIndicesFieldTypesProvider from 'plugins/kibana/settings/sections/indices/_field_types'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import editTemplate from 'plugins/kibana/settings/sections/indices/_edit.html'; - require('ui/routes') - .when('/settings/indices/:indexPatternId', { - template: require('plugins/kibana/settings/sections/indices/_edit.html'), - resolve: { - indexPattern: function ($route, courier) { - return courier.indexPatterns.get($route.current.params.indexPatternId) - .catch(courier.redirectWhenMissing('/settings/indices')); - } +uiRoutes +.when('/settings/indices/:indexPatternId', { + template: editTemplate, + resolve: { + indexPattern: function ($route, courier) { + return courier.indexPatterns.get($route.current.params.indexPatternId) + .catch(courier.redirectWhenMissing('/settings/indices')); } - }); + } +}); - require('ui/modules').get('apps/settings') - .controller('settingsIndicesEdit', function ($scope, $location, $route, config, courier, Notifier, Private, AppState, docTitle) { +uiModules.get('apps/settings') +.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/kibana/settings/sections/indices/_refresh_kibana_index')); + const notify = new Notifier(); + const $state = $scope.state = new AppState(); + const refreshKibanaIndex = Private(PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider); - $scope.kbnUrl = Private(require('ui/url')); - $scope.indexPattern = $route.current.locals.indexPattern; - docTitle.change($scope.indexPattern.id); - var otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id); + $scope.kbnUrl = Private(UrlProvider); + $scope.indexPattern = $route.current.locals.indexPattern; + docTitle.change($scope.indexPattern.id); + const otherIds = _.without($route.current.locals.indexPatternIds, $scope.indexPattern.id); - var fieldTypes = Private(require('plugins/kibana/settings/sections/indices/_field_types')); - $scope.$watch('indexPattern.fields', function () { - $scope.fieldTypes = fieldTypes($scope.indexPattern); - }); + const fieldTypes = Private(PluginsKibanaSettingsSectionsIndicesFieldTypesProvider); + $scope.$watch('indexPattern.fields', function () { + $scope.fieldTypes = fieldTypes($scope.indexPattern); + }); - $scope.changeTab = function (obj) { - $state.tab = obj.index; - $state.save(); - }; + $scope.changeTab = function (obj) { + $state.tab = obj.index; + $state.save(); + }; - $scope.$watch('state.tab', function (tab) { - if (!tab) $scope.changeTab($scope.fieldTypes[0]); - }); + $scope.$watch('state.tab', function (tab) { + if (!tab) $scope.changeTab($scope.fieldTypes[0]); + }); - $scope.$watchCollection('indexPattern.fields', function () { - $scope.conflictFields = _.filter($scope.indexPattern.fields, {type: 'conflict'}); - }); + $scope.$watchCollection('indexPattern.fields', function () { + $scope.conflictFields = _.filter($scope.indexPattern.fields, {type: 'conflict'}); + }); - $scope.refreshFields = function () { - $scope.indexPattern.refreshFields(); - }; + $scope.refreshFields = function () { + $scope.indexPattern.refreshFields(); + }; - $scope.removePattern = function () { - if ($scope.indexPattern.id === config.get('defaultIndex')) { - config.delete('defaultIndex'); - if (otherIds.length) { - config.set('defaultIndex', otherIds[0]); - } + $scope.removePattern = function () { + if ($scope.indexPattern.id === config.get('defaultIndex')) { + config.remove('defaultIndex'); + if (otherIds.length) { + config.set('defaultIndex', otherIds[0]); } + } - courier.indexPatterns.delete($scope.indexPattern) - .then(refreshKibanaIndex) - .then(function () { - $location.url('/settings/indices'); - }) - .catch(notify.fatal); - }; + courier.indexPatterns.delete($scope.indexPattern) + .then(refreshKibanaIndex) + .then(function () { + $location.url('/settings/indices'); + }) + .catch(notify.fatal); + }; - $scope.setDefaultPattern = function () { - config.set('defaultIndex', $scope.indexPattern.id); - }; + $scope.setDefaultPattern = function () { + config.set('defaultIndex', $scope.indexPattern.id); + }; - $scope.setIndexPatternsTimeField = function (field) { - if (field.type !== 'date') { - notify.error('That field is a ' + field.type + ' not a date.'); - return; - } - $scope.indexPattern.timeFieldName = field.name; - return $scope.indexPattern.save(); - }; - }); + $scope.setIndexPatternsTimeField = function (field) { + if (field.type !== 'date') { + notify.error('That field is a ' + field.type + ' not a date.'); + return; + } + $scope.indexPattern.timeFieldName = field.name; + return $scope.indexPattern.save(); + }; }); diff --git a/src/plugins/kibana/public/settings/sections/indices/_field_editor.js b/src/plugins/kibana/public/settings/sections/indices/_field_editor.js index a93db7f716542..45ca674046275 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_field_editor.js +++ b/src/plugins/kibana/public/settings/sections/indices/_field_editor.js @@ -1,55 +1,57 @@ -define(function (require) { - require('ui/field_editor'); - require('plugins/kibana/settings/sections/indices/_index_header'); - - require('ui/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('plugins/kibana/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('ui/index_patterns/_field')); - var notify = new Notifier({ location: 'Field Editor' }); - var kbnUrl = Private(require('ui/url')); - +import 'ui/field_editor'; +import 'plugins/kibana/settings/sections/indices/_index_header'; +import IndexPatternsFieldProvider from 'ui/index_patterns/_field'; +import UrlProvider from 'ui/url'; +import uiRoutes from 'ui/routes'; +import fieldEditorTemplate from 'plugins/kibana/settings/sections/indices/_field_editor.html'; + +uiRoutes +.when('/settings/indices/:indexPatternId/field/:fieldName', { mode: 'edit' }) +.when('/settings/indices/:indexPatternId/create-field/', { mode: 'create' }) +.defaults(/settings\/indices\/[^\/]+\/(field|create-field)(\/|$)/, { + template: fieldEditorTemplate, + 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) { + const Field = Private(IndexPatternsFieldProvider); + const notify = new Notifier({ location: 'Field Editor' }); + const kbnUrl = Private(UrlProvider); - this.mode = $route.current.mode; - this.indexPattern = $route.current.locals.indexPattern; + 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; - } + if (this.mode === 'edit') { + const fieldName = $route.current.params.fieldName; + this.field = this.indexPattern.fields.byName[fieldName]; - } - else if (this.mode === 'create') { - this.field = new Field(this.indexPattern, { - scripted: true, - type: 'number' - }); - } - else { - throw new Error('unknown fieldSettings mode ' + this.mode); + if (!this.field) { + notify.error(this.indexPattern + ' does not have a "' + fieldName + '" field.'); + kbnUrl.redirectToRoute(this.indexPattern, 'edit'); + return; } - docTitle.change([this.field.name || 'New Scripted Field', this.indexPattern.id]); - this.goBack = function () { - kbnUrl.changeToRoute(this.indexPattern, 'edit'); - }; } - }); + 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'); + }; + } }); + diff --git a/src/plugins/kibana/public/settings/sections/indices/_field_types.js b/src/plugins/kibana/public/settings/sections/indices/_field_types.js index 6e4e1f284315c..b1336ce1c6973 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_field_types.js +++ b/src/plugins/kibana/public/settings/sections/indices/_field_types.js @@ -1,26 +1,24 @@ -define(function (require) { - return function GetFieldTypes() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function GetFieldTypes() { - return function (indexPattern) { - var fieldCount = _.countBy(indexPattern.fields, function (field) { - return (field.scripted) ? 'scripted' : 'indexed'; - }); + return function (indexPattern) { + const fieldCount = _.countBy(indexPattern.fields, function (field) { + return (field.scripted) ? 'scripted' : 'indexed'; + }); - _.defaults(fieldCount, { - indexed: 0, - scripted: 0 - }); + _.defaults(fieldCount, { + indexed: 0, + scripted: 0 + }); - return [{ - title: 'fields', - index: 'indexedFields', - count: fieldCount.indexed - }, { - title: 'scripted fields', - index: 'scriptedFields', - count: fieldCount.scripted - }]; - }; + return [{ + title: 'fields', + index: 'indexedFields', + count: fieldCount.indexed + }, { + title: 'scripted fields', + index: 'scriptedFields', + count: fieldCount.scripted + }]; }; -}); +}; diff --git a/src/plugins/kibana/public/settings/sections/indices/_index_header.js b/src/plugins/kibana/public/settings/sections/indices/_index_header.js index e079aae64a003..f16156d727e9f 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_index_header.js +++ b/src/plugins/kibana/public/settings/sections/indices/_index_header.js @@ -1,22 +1,22 @@ -define(function (require) { - require('ui/modules') - .get('apps/settings') - .directive('kbnSettingsIndexHeader', function (config) { - return { - restrict: 'E', - template: require('plugins/kibana/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'); - } - }; - }); +import uiModules from 'ui/modules'; +import indexHeaderTemplate from 'plugins/kibana/settings/sections/indices/_index_header.html'; +uiModules +.get('apps/settings') +.directive('kbnSettingsIndexHeader', function (config) { + return { + restrict: 'E', + template: indexHeaderTemplate, + 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.bindToScope($scope, 'defaultIndex'); + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/indices/_indexed_fields.js b/src/plugins/kibana/public/settings/sections/indices/_indexed_fields.js index cebb4f10e6009..1d0c6f4904223 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_indexed_fields.js +++ b/src/plugins/kibana/public/settings/sections/indices/_indexed_fields.js @@ -1,73 +1,73 @@ -define(function (require) { - var _ = require('lodash'); - require('ui/paginated_table'); +import _ from 'lodash'; +import 'ui/paginated_table'; +import nameHtml from 'plugins/kibana/settings/sections/indices/_field_name.html'; +import typeHtml from 'plugins/kibana/settings/sections/indices/_field_type.html'; +import controlsHtml from 'plugins/kibana/settings/sections/indices/_field_controls.html'; +import uiModules from 'ui/modules'; +import indexedFieldsTemplate from 'plugins/kibana/settings/sections/indices/_indexed_fields.html'; - require('ui/modules').get('apps/settings') - .directive('indexedFields', function ($filter) { - var yesTemplate = ''; - var noTemplate = ''; - var nameHtml = require('plugins/kibana/settings/sections/indices/_field_name.html'); - var typeHtml = require('plugins/kibana/settings/sections/indices/_field_type.html'); - var controlsHtml = require('plugins/kibana/settings/sections/indices/_field_controls.html'); - var filter = $filter('filter'); +uiModules.get('apps/settings') +.directive('indexedFields', function ($filter) { + const yesTemplate = ''; + const noTemplate = ''; + const filter = $filter('filter'); - return { - restrict: 'E', - template: require('plugins/kibana/settings/sections/indices/_indexed_fields.html'), - scope: true, - link: function ($scope) { - var rowScopes = []; // track row scopes, so they can be destroyed as needed - $scope.perPage = 25; - $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: 'controls', sortable: false } - ]; + return { + restrict: 'E', + template: indexedFieldsTemplate, + scope: true, + link: function ($scope) { + const rowScopes = []; // track row scopes, so they can be destroyed as needed + $scope.perPage = 25; + $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: 'controls', sortable: false } + ]; - $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows); + $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows); - function refreshRows() { - // clear and destroy row scopes - _.invoke(rowScopes.splice(0), '$destroy'); + function refreshRows() { + // clear and destroy row scopes + _.invoke(rowScopes.splice(0), '$destroy'); - var fields = filter($scope.indexPattern.getNonScriptedFields(), $scope.fieldFilter); - _.find($scope.fieldTypes, {index: 'indexedFields'}).count = fields.length; // Update the tab count + const fields = filter($scope.indexPattern.getNonScriptedFields(), $scope.fieldFilter); + _.find($scope.fieldTypes, {index: 'indexedFields'}).count = fields.length; // Update the tab count - $scope.rows = fields.map(function (field) { - var childScope = _.assign($scope.$new(), { field: field }); - rowScopes.push(childScope); + $scope.rows = fields.map(function (field) { + const childScope = _.assign($scope.$new(), { field: field }); + rowScopes.push(childScope); - return [ - { - markup: nameHtml, - scope: childScope, - value: field.displayName - }, - { - markup: typeHtml, - scope: childScope, - value: field.type - }, - _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), - { - markup: field.analyzed ? yesTemplate : noTemplate, - value: field.analyzed - }, - { - markup: field.indexed ? yesTemplate : noTemplate, - value: field.indexed - }, - { - markup: controlsHtml, - scope: childScope - } - ]; - }); - } + return [ + { + markup: nameHtml, + scope: childScope, + value: field.displayName + }, + { + markup: typeHtml, + scope: childScope, + value: field.type + }, + _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), + { + markup: field.analyzed ? yesTemplate : noTemplate, + value: field.analyzed + }, + { + markup: field.indexed ? yesTemplate : noTemplate, + value: field.indexed + }, + { + markup: controlsHtml, + scope: childScope + } + ]; + }); } - }; - }); + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/indices/_refresh_kibana_index.js b/src/plugins/kibana/public/settings/sections/indices/_refresh_kibana_index.js index 190cc961815d2..e0becc475ab43 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_refresh_kibana_index.js +++ b/src/plugins/kibana/public/settings/sections/indices/_refresh_kibana_index.js @@ -1,9 +1,7 @@ -define(function (require) { - return function RefreshKibanaIndexFn(es, kbnIndex) { - return function () { - return es.indices.refresh({ - index: kbnIndex - }); - }; +export default function RefreshKibanaIndexFn(es, kbnIndex) { + return function () { + return es.indices.refresh({ + index: kbnIndex + }); }; -}); +}; diff --git a/src/plugins/kibana/public/settings/sections/indices/_scripted_fields.js b/src/plugins/kibana/public/settings/sections/indices/_scripted_fields.js index 7c9ecf1b4b7f5..da6f4b3ebef00 100644 --- a/src/plugins/kibana/public/settings/sections/indices/_scripted_fields.js +++ b/src/plugins/kibana/public/settings/sections/indices/_scripted_fields.js @@ -1,102 +1,102 @@ -define(function (require) { - var _ = require('lodash'); - require('ui/paginated_table'); - - require('ui/modules').get('apps/settings') - .directive('scriptedFields', function (kbnUrl, Notifier, $filter) { - var rowScopes = []; // track row scopes, so they can be destroyed as needed - var popularityHtml = require('plugins/kibana/settings/sections/indices/_field_popularity.html'); - var controlsHtml = require('plugins/kibana/settings/sections/indices/_field_controls.html'); - var filter = $filter('filter'); - - var notify = new Notifier(); - - return { - restrict: 'E', - template: require('plugins/kibana/settings/sections/indices/_scripted_fields.html'), - scope: true, - link: function ($scope) { - var dateScripts = require('plugins/kibana/settings/sections/indices/_date_scripts'); - - var fieldCreatorPath = '/settings/indices/{{ indexPattern }}/scriptedField'; - var fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}'; - - $scope.perPage = 25; - $scope.columns = [ - { title: 'name' }, - { title: 'script' }, - { title: 'format' }, - { title: 'controls', sortable: false } - ]; - - $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows); - - function refreshRows() { - _.invoke(rowScopes, '$destroy'); - rowScopes.length = 0; - - var fields = filter($scope.indexPattern.getScriptedFields(), $scope.fieldFilter); - _.find($scope.fieldTypes, {index: 'scriptedFields'}).count = fields.length; // Update the tab count - - $scope.rows = fields.map(function (field) { - var rowScope = $scope.$new(); - rowScope.field = field; - rowScopes.push(rowScope); - - return [ - field.name, - field.script, - _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), - { - markup: controlsHtml, - scope: rowScope - } - ]; - }); - } - - $scope.addDateScripts = function () { - var conflictFields = []; - var fieldsAdded = 0; - _.each(dateScripts($scope.indexPattern), function (script, field) { - try { - $scope.indexPattern.addScriptedField(field, script, 'number'); - fieldsAdded++; - } catch (e) { - conflictFields.push(field); +import _ from 'lodash'; +import 'ui/paginated_table'; +import popularityHtml from 'plugins/kibana/settings/sections/indices/_field_popularity.html'; +import controlsHtml from 'plugins/kibana/settings/sections/indices/_field_controls.html'; +import dateScripts from 'plugins/kibana/settings/sections/indices/_date_scripts'; +import uiModules from 'ui/modules'; +import scriptedFieldsTemplate from 'plugins/kibana/settings/sections/indices/_scripted_fields.html'; + +uiModules.get('apps/settings') +.directive('scriptedFields', function (kbnUrl, Notifier, $filter) { + const rowScopes = []; // track row scopes, so they can be destroyed as needed + const filter = $filter('filter'); + + const notify = new Notifier(); + + return { + restrict: 'E', + template: scriptedFieldsTemplate, + scope: true, + link: function ($scope) { + + const fieldCreatorPath = '/settings/indices/{{ indexPattern }}/scriptedField'; + const fieldEditorPath = fieldCreatorPath + '/{{ fieldName }}'; + + $scope.perPage = 25; + $scope.columns = [ + { title: 'name' }, + { title: 'script' }, + { title: 'format' }, + { title: 'controls', sortable: false } + ]; + + $scope.$watchMulti(['[]indexPattern.fields', 'fieldFilter'], refreshRows); + + function refreshRows() { + _.invoke(rowScopes, '$destroy'); + rowScopes.length = 0; + + const fields = filter($scope.indexPattern.getScriptedFields(), $scope.fieldFilter); + _.find($scope.fieldTypes, {index: 'scriptedFields'}).count = fields.length; // Update the tab count + + $scope.rows = fields.map(function (field) { + const rowScope = $scope.$new(); + rowScope.field = field; + rowScopes.push(rowScope); + + return [ + _.escape(field.name), + _.escape(field.script), + _.get($scope.indexPattern, ['fieldFormatMap', field.name, 'type', 'title']), + { + markup: controlsHtml, + scope: rowScope } - }); + ]; + }); + } - if (fieldsAdded > 0) { - notify.info(fieldsAdded + ' script fields created'); + $scope.addDateScripts = function () { + const conflictFields = []; + let fieldsAdded = 0; + _.each(dateScripts($scope.indexPattern), function (script, field) { + try { + $scope.indexPattern.addScriptedField(field, script, 'number'); + fieldsAdded++; + } catch (e) { + conflictFields.push(field); } + }); - if (conflictFields.length > 0) { - notify.info('Not adding ' + conflictFields.length + ' duplicate fields: ' + conflictFields.join(', ')); - } - }; + if (fieldsAdded > 0) { + notify.info(fieldsAdded + ' script fields created'); + } - $scope.create = function () { - var params = { - indexPattern: $scope.indexPattern.id - }; + if (conflictFields.length > 0) { + notify.info('Not adding ' + conflictFields.length + ' duplicate fields: ' + conflictFields.join(', ')); + } + }; - kbnUrl.change(fieldCreatorPath, params); + $scope.create = function () { + const params = { + indexPattern: $scope.indexPattern.id }; - $scope.edit = function (field) { - var params = { - indexPattern: $scope.indexPattern.id, - fieldName: field.name - }; + kbnUrl.change(fieldCreatorPath, params); + }; - kbnUrl.change(fieldEditorPath, params); + $scope.edit = function (field) { + const params = { + indexPattern: $scope.indexPattern.id, + fieldName: field.name }; - $scope.remove = function (field) { - $scope.indexPattern.removeScriptedField(field.name); - }; - } - }; - }); + kbnUrl.change(fieldEditorPath, params); + }; + + $scope.remove = function (field) { + $scope.indexPattern.removeScriptedField(field.name); + }; + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/indices/index.html b/src/plugins/kibana/public/settings/sections/indices/index.html index 15809f900a47e..755db8397209d 100644 --- a/src/plugins/kibana/public/settings/sections/indices/index.html +++ b/src/plugins/kibana/public/settings/sections/indices/index.html @@ -4,7 +4,7 @@
Index Patterns  diff --git a/src/plugins/kibana/public/settings/sections/indices/index.js b/src/plugins/kibana/public/settings/sections/indices/index.js index b3588ef40bd72..9670fbdfad498 100644 --- a/src/plugins/kibana/public/settings/sections/indices/index.js +++ b/src/plugins/kibana/public/settings/sections/indices/index.js @@ -1,51 +1,54 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import registry from 'ui/registry/settings_sections'; +import 'plugins/kibana/settings/sections/indices/_create'; +import 'plugins/kibana/settings/sections/indices/_edit'; +import 'plugins/kibana/settings/sections/indices/_field_editor'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import indexTemplate from 'plugins/kibana/settings/sections/indices/index.html'; - require('plugins/kibana/settings/sections/indices/_create'); - require('plugins/kibana/settings/sections/indices/_edit'); - require('plugins/kibana/settings/sections/indices/_field_editor'); - // add a dependency to all of the subsection routes - require('ui/routes') - .defaults(/settings\/indices/, { - resolve: { - indexPatternIds: function (courier) { - return courier.indexPatterns.getIds(); - } +// add a dependency to all of the subsection routes +uiRoutes +.defaults(/settings\/indices/, { + resolve: { + indexPatternIds: function (courier) { + return courier.indexPatterns.getIds(); } - }); + } +}); - // wrapper directive, which sets some global stuff up like the left nav - require('ui/modules').get('apps/settings') - .directive('kbnSettingsIndices', function ($route, config, kbnUrl) { - return { - restrict: 'E', - transclude: true, - template: require('plugins/kibana/settings/sections/indices/index.html'), - link: function ($scope) { - $scope.edittingId = $route.current.params.indexPatternId; - config.$bind($scope, 'defaultIndex'); +// wrapper directive, which sets some global stuff up like the left nav +uiModules.get('apps/settings') +.directive('kbnSettingsIndices', function ($route, config, kbnUrl) { + return { + restrict: 'E', + transclude: true, + template: indexTemplate, + link: function ($scope) { + $scope.editingId = $route.current.params.indexPatternId; + config.bindToScope($scope, 'defaultIndex'); - $scope.$watch('defaultIndex', function () { - var ids = $route.current.locals.indexPatternIds; - $scope.indexPatternList = ids.map(function (id) { - return { - id: id, - url: kbnUrl.eval('#/settings/indices/{{id}}', {id: id}), - class: 'sidebar-item-title ' + ($scope.edittingId === id ? 'active' : ''), - default: $scope.defaultIndex === id - }; - }); + $scope.$watch('defaultIndex', function () { + const ids = $route.current.locals.indexPatternIds; + $scope.indexPatternList = ids.map(function (id) { + return { + id: id, + url: kbnUrl.eval('#/settings/indices/{{id}}', {id: id}), + class: 'sidebar-item-title ' + ($scope.editingId === id ? 'active' : ''), + default: $scope.defaultIndex === id + }; }); + }); - $scope.$emit('application.load'); - } - }; - }); - - return { - name: 'indices', - display: 'Indices', - url: '#/settings/indices', + $scope.$emit('application.load'); + } }; }); + +registry.register(_.constant({ + order: 1, + name: 'indices', + display: 'Indices', + url: '#/settings/indices' +})); diff --git a/src/plugins/kibana/public/settings/sections/objects/_objects.html b/src/plugins/kibana/public/settings/sections/objects/_objects.html index 3c1da5cd909ad..550ef9aec945f 100644 --- a/src/plugins/kibana/public/settings/sections/objects/_objects.html +++ b/src/plugins/kibana/public/settings/sections/objects/_objects.html @@ -3,7 +3,11 @@

Edit Saved Objects

- + + +

From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen. Each tab is limited to 100 results. You can use the filter to find objects not in the default list. @@ -29,7 +33,7 @@

Edit Saved Objects

Delete = 0) { - $scope.selectedItems.splice(i, 1); - } else { - $scope.selectedItems.push(item); - } - }; + $scope.$watch('state.tab', function (tab) { + if (!tab) $scope.changeTab($scope.services[0]); + }); + }); + }; - $scope.open = function (item) { - kbnUrl.change(item.url.substr(1)); - }; - $scope.edit = function (service, item) { - var params = { - service: service.serviceName, - id: item.id - }; + $scope.toggleAll = function () { + if ($scope.selectedItems.length === $scope.currentTab.data.length) { + $scope.selectedItems.length = 0; + } else { + $scope.selectedItems = [].concat($scope.currentTab.data); + } + }; + + $scope.toggleItem = function (item) { + const i = $scope.selectedItems.indexOf(item); + if (i >= 0) { + $scope.selectedItems.splice(i, 1); + } else { + $scope.selectedItems.push(item); + } + }; - kbnUrl.change('/settings/objects/{{ service }}/{{ id }}', params); - }; + $scope.open = function (item) { + kbnUrl.change(item.url.substr(1)); + }; - $scope.bulkDelete = function () { - $scope.currentTab.service.delete(_.pluck($scope.selectedItems, 'id')).then(refreshData).then(function () { - $scope.selectedItems.length = 0; - }); + $scope.edit = function (service, item) { + const params = { + service: service.serviceName, + id: item.id }; - $scope.bulkExport = function () { - var objs = $scope.selectedItems.map(_.partialRight(_.extend, {type: $scope.currentTab.type})); - retrieveAndExportDocs(objs); - }; + kbnUrl.change('/settings/objects/{{ service }}/{{ id }}', params); + }; - $scope.exportAll = () => { - Promise.map($scope.services, (service) => - service.service.find('', MAX_SIZE).then((results) => - results.hits.map((hit) => _.extend(hit, {type: service.type})) - ) - ).then((results) => retrieveAndExportDocs(_.flattenDeep(results))); - }; + $scope.bulkDelete = function () { + $scope.currentTab.service.delete(pluck($scope.selectedItems, 'id')) + .then(refreshData) + .then(function () { + $scope.selectedItems.length = 0; + }) + .catch(error => notify.error(error)); + }; + + $scope.bulkExport = function () { + const objs = $scope.selectedItems.map(partialRight(extend, {type: $scope.currentTab.type})); + retrieveAndExportDocs(objs); + }; + + $scope.exportAll = () => Promise + .map($scope.services, service => service.service + .scanAll('') + .then(result => result.hits.map(hit => extend(hit, { type: service.type }))) + ) + .then(results => retrieveAndExportDocs(flattenDeep(results))) + .catch(error => notify.error(error)); + + function retrieveAndExportDocs(objs) { + if (!objs.length) return notify.error('No saved objects to export.'); + es.mget({ + index: kbnIndex, + body: {docs: objs.map(transformToMget)} + }) + .then(function (response) { + saveToFile(response.docs.map(partialRight(pick, '_id', '_type', '_source'))); + }); + } - function retrieveAndExportDocs(objs) { - es.mget({ - index: kbnIndex, - body: {docs: objs.map(transformToMget)} - }) - .then(function (response) { - saveToFile(response.docs.map(_.partialRight(_.pick, '_id', '_type', '_source'))); - }); - } + // Takes an object and returns the associated data needed for an mget API request + function transformToMget(obj) { + return {_id: obj.id, _type: obj.type}; + } - // Takes an object and returns the associated data needed for an mget API request - function transformToMget(obj) { - return {_id: obj.id, _type: obj.type}; - } + function saveToFile(results) { + const blob = new Blob([angular.toJson(results, true)], {type: 'application/json'}); + saveAs(blob, 'export.json'); + } - function saveToFile(results) { - var blob = new Blob([angular.toJson(results, true)], {type: 'application/json'}); - saveAs(blob, 'export.json'); + $scope.importAll = function (fileContents) { + let docs; + try { + docs = JSON.parse(fileContents); + } catch (e) { + notify.error('The file could not be processed.'); } - $scope.importAll = function (fileContents) { - var docs; - try { - docs = JSON.parse(fileContents); - } catch (e) { - notify.error('The file could not be processed.'); - } - - return Promise.map(docs, function (doc) { - var service = _.find($scope.services, {type: doc._type}).service; - return service.get().then(function (obj) { - obj.id = doc._id; - return obj.applyESResp(doc).then(function () { - return obj.save(); - }); + return Promise.map(docs, function (doc) { + const service = find($scope.services, {type: doc._type}).service; + return service.get().then(function (obj) { + obj.id = doc._id; + return obj.applyESResp(doc).then(function () { + return obj.save(); }); - }) - .then(refreshIndex) - .then(refreshData, notify.error); - }; - - function refreshIndex() { - return es.indices.refresh({ - index: kbnIndex }); - } - - function refreshData() { - return getData($scope.advancedFilter); - } - - $scope.changeTab = function (tab) { - $scope.currentTab = tab; - $scope.selectedItems.length = 0; - $state.tab = tab.title; - $state.save(); - }; - - $scope.$watch('advancedFilter', function (filter) { - getData(filter); + }) + .then(refreshIndex) + .then(refreshData, notify.error); + }; + + function refreshIndex() { + return es.indices.refresh({ + index: kbnIndex }); } - }; - }); + + function refreshData() { + return getData($scope.advancedFilter); + } + + $scope.changeTab = function (tab) { + $scope.currentTab = tab; + $scope.selectedItems.length = 0; + $state.tab = tab.title; + $state.save(); + }; + + $scope.$watch('advancedFilter', function (filter) { + getData(filter); + }); + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/objects/_view.js b/src/plugins/kibana/public/settings/sections/objects/_view.js index a78fe2bd77b4b..f3f54f97cfea6 100644 --- a/src/plugins/kibana/public/settings/sections/objects/_view.js +++ b/src/plugins/kibana/public/settings/sections/objects/_view.js @@ -1,222 +1,223 @@ -define(function (require) { - var _ = require('lodash'); - var angular = require('angular'); - var rison = require('ui/utils/rison'); - var registry = require('plugins/kibana/settings/saved_object_registry'); - var objectViewHTML = require('plugins/kibana/settings/sections/objects/_view.html'); - - require('ui/routes') - .when('/settings/objects/:service/:id', { - template: objectViewHTML - }); - - require('ui/modules').get('apps/settings') - .directive('kbnSettingsObjectsView', function (kbnIndex, Notifier) { - return { - restrict: 'E', - controller: function ($scope, $injector, $routeParams, $location, $window, $rootScope, es, Private) { - var notify = new Notifier({ location: 'SavedObject view' }); - var castMappingType = Private(require('ui/index_patterns/_cast_mapping_type')); - var serviceObj = registry.get($routeParams.service); - var service = $injector.get(serviceObj.service); - - /** - * Creates a field definition and pushes it to the memo stack. This function - * is designed to be used in conjunction with _.reduce(). If the - * values is plain object it will recurse through all the keys till it hits - * a string, number or an array. - * - * @param {array} memo The stack of fields - * @param {mixed} value The value of the field - * @param {string} key The key of the field - * @param {object} collection This is a reference the collection being reduced - * @param {array} parents The parent keys to the field - * @returns {array} - */ - var createField = function (memo, val, key, collection, parents) { - if (_.isArray(parents)) { - parents.push(key); - } else { - parents = [key]; - } +import _ from 'lodash'; +import angular from 'angular'; +import rison from 'rison-node'; +import registry from 'plugins/kibana/settings/saved_object_registry'; +import objectViewHTML from 'plugins/kibana/settings/sections/objects/_view.html'; +import IndexPatternsCastMappingTypeProvider from 'ui/index_patterns/_cast_mapping_type'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; + +uiRoutes +.when('/settings/objects/:service/:id', { + template: objectViewHTML +}); - var field = { type: 'text', name: parents.join('.'), value: val }; +uiModules.get('apps/settings') +.directive('kbnSettingsObjectsView', function (kbnIndex, Notifier) { + return { + restrict: 'E', + controller: function ($scope, $injector, $routeParams, $location, $window, $rootScope, es, Private) { + const notify = new Notifier({ location: 'SavedObject view' }); + const castMappingType = Private(IndexPatternsCastMappingTypeProvider); + const serviceObj = registry.get($routeParams.service); + const service = $injector.get(serviceObj.service); + + /** + * Creates a field definition and pushes it to the memo stack. This function + * is designed to be used in conjunction with _.reduce(). If the + * values is plain object it will recurse through all the keys till it hits + * a string, number or an array. + * + * @param {array} memo The stack of fields + * @param {mixed} value The value of the field + * @param {string} key The key of the field + * @param {object} collection This is a reference the collection being reduced + * @param {array} parents The parent keys to the field + * @returns {array} + */ + const createField = function (memo, val, key, collection, parents) { + if (_.isArray(parents)) { + parents.push(key); + } else { + parents = [key]; + } - if (_.isString(field.value)) { - try { - field.value = angular.toJson(JSON.parse(field.value), true); - field.type = 'json'; - } catch (err) { - field.value = field.value; - } - } else if (_.isNumeric(field.value)) { - field.type = 'number'; - } else if (_.isArray(field.value)) { - field.type = 'array'; - field.value = angular.toJson(field.value, true); - } else if (_.isBoolean(field.value)) { - field.type = 'boolean'; + const field = { type: 'text', name: parents.join('.'), value: val }; + + if (_.isString(field.value)) { + try { + field.value = angular.toJson(JSON.parse(field.value), true); + field.type = 'json'; + } catch (err) { field.value = field.value; - } else if (_.isPlainObject(field.value)) { - // do something recursive - return _.reduce(field.value, _.partialRight(createField, parents), memo); } + } else if (_.isNumeric(field.value)) { + field.type = 'number'; + } else if (_.isArray(field.value)) { + field.type = 'array'; + field.value = angular.toJson(field.value, true); + } else if (_.isBoolean(field.value)) { + field.type = 'boolean'; + field.value = field.value; + } else if (_.isPlainObject(field.value)) { + // do something recursive + return _.reduce(field.value, _.partialRight(createField, parents), memo); + } - memo.push(field); - - // once the field is added to the object you need to pop the parents - // to remove it since we've hit the end of the branch. - parents.pop(); - return memo; - }; - - var readObjectClass = function (fields, Class) { - var fieldMap = _.indexBy(fields, 'name'); - - _.forOwn(Class.mapping, function (esType, name) { - if (fieldMap[name]) return; - - fields.push({ - name: name, - type: (function () { - switch (castMappingType(esType)) { - case 'string': return 'text'; - case 'number': return 'number'; - case 'boolean': return 'boolean'; - default: return 'json'; - } - }()) - }); - }); + memo.push(field); - if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) { - fields.push({ - name: 'kibanaSavedObjectMeta.searchSourceJSON', - type: 'json', - value: '{}' - }); - } - }; + // once the field is added to the object you need to pop the parents + // to remove it since we've hit the end of the branch. + parents.pop(); + return memo; + }; - $scope.notFound = $routeParams.notFound; + const readObjectClass = function (fields, Class) { + const fieldMap = _.indexBy(fields, 'name'); - $scope.title = service.type; + _.forOwn(Class.mapping, function (esType, name) { + if (fieldMap[name]) return; - es.get({ + fields.push({ + name: name, + type: (function () { + switch (castMappingType(esType)) { + case 'string': return 'text'; + case 'number': return 'number'; + case 'boolean': return 'boolean'; + default: return 'json'; + } + }()) + }); + }); + + if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) { + fields.push({ + name: 'kibanaSavedObjectMeta.searchSourceJSON', + type: 'json', + value: '{}' + }); + } + }; + + $scope.notFound = $routeParams.notFound; + + $scope.title = service.type; + + es.get({ + index: kbnIndex, + type: service.type, + id: $routeParams.id + }) + .then(function (obj) { + $scope.obj = obj; + $scope.link = service.urlFor(obj._id); + + const fields = _.reduce(obj._source, createField, []); + if (service.Class) readObjectClass(fields, service.Class); + $scope.fields = _.sortBy(fields, 'name'); + }) + .catch(notify.fatal); + + // This handles the validation of the Ace Editor. Since we don't have any + // other hooks into the editors to tell us if the content is valid or not + // we need to use the annotations to see if they have any errors. If they + // do then we push the field.name to aceInvalidEditor variable. + // Otherwise we remove it. + const loadedEditors = []; + $scope.aceInvalidEditors = []; + + $scope.aceLoaded = function (editor) { + if (_.contains(loadedEditors, editor)) return; + loadedEditors.push(editor); + + editor.$blockScrolling = Infinity; + + const session = editor.getSession(); + const fieldName = editor.container.id; + + session.setTabSize(2); + session.setUseSoftTabs(true); + session.on('changeAnnotation', function () { + const annotations = session.getAnnotations(); + if (_.some(annotations, { type: 'error'})) { + if (!_.contains($scope.aceInvalidEditors, fieldName)) { + $scope.aceInvalidEditors.push(fieldName); + } + } else { + $scope.aceInvalidEditors = _.without($scope.aceInvalidEditors, fieldName); + } + + if ($rootScope.$$phase) $scope.$apply(); + }); + }; + + $scope.cancel = function () { + $window.history.back(); + return false; + }; + + /** + * Deletes an object and sets the notification + * @param {type} name description + * @returns {type} description + */ + $scope.delete = function () { + es.delete({ index: kbnIndex, type: service.type, id: $routeParams.id }) - .then(function (obj) { - $scope.obj = obj; - $scope.link = service.urlFor(obj._id); - - var fields = _.reduce(obj._source, createField, []); - if (service.Class) readObjectClass(fields, service.Class); - $scope.fields = _.sortBy(fields, 'name'); + .then(function (resp) { + return redirectHandler('deleted'); }) .catch(notify.fatal); + }; - // This handles the validation of the Ace Editor. Since we don't have any - // other hooks into the editors to tell us if the content is valid or not - // we need to use the annotations to see if they have any errors. If they - // do then we push the field.name to aceInvalidEditor variable. - // Otherwise we remove it. - var loadedEditors = []; - $scope.aceInvalidEditors = []; - - $scope.aceLoaded = function (editor) { - if (_.contains(loadedEditors, editor)) return; - loadedEditors.push(editor); - - editor.$blockScrolling = Infinity; - - var session = editor.getSession(); - var fieldName = editor.container.id; - - session.setTabSize(2); - session.setUseSoftTabs(true); - session.on('changeAnnotation', function () { - var annotations = session.getAnnotations(); - if (_.some(annotations, { type: 'error'})) { - if (!_.contains($scope.aceInvalidEditors, fieldName)) { - $scope.aceInvalidEditors.push(fieldName); - } - } else { - $scope.aceInvalidEditors = _.without($scope.aceInvalidEditors, fieldName); - } + $scope.submit = function () { + const source = _.cloneDeep($scope.obj._source); - if ($rootScope.$$phase) $scope.$apply(); - }); - }; - - $scope.cancel = function () { - $window.history.back(); - return false; - }; - - /** - * Deletes an object and sets the notification - * @param {type} name description - * @returns {type} description - */ - $scope.delete = function () { - es.delete({ - index: kbnIndex, - type: service.type, - id: $routeParams.id - }) - .then(function (resp) { - return redirectHandler('deleted'); - }) - .catch(notify.fatal); - }; - - $scope.submit = function () { - var source = _.cloneDeep($scope.obj._source); - - _.each($scope.fields, function (field) { - var value = field.value; - - if (field.type === 'number') { - value = Number(field.value); - } + _.each($scope.fields, function (field) { + let value = field.value; - if (field.type === 'array') { - value = JSON.parse(field.value); - } + if (field.type === 'number') { + value = Number(field.value); + } - _.set(source, field.name, value); - }); + if (field.type === 'array') { + value = JSON.parse(field.value); + } + + _.set(source, field.name, value); + }); - es.index({ - index: kbnIndex, - type: service.type, - id: $routeParams.id, - body: source - }) - .then(function (resp) { - return redirectHandler('updated'); - }) - .catch(notify.fatal); - }; - - function redirectHandler(action) { - return es.indices.refresh({ - index: kbnIndex - }) - .then(function (resp) { - var msg = 'You successfully ' + action + ' the "' + $scope.obj._source.title + '" ' + $scope.title.toLowerCase() + ' object'; - - $location.path('/settings/objects').search({ - _a: rison.encode({ - tab: serviceObj.title - }) - }); - notify.info(msg); + es.index({ + index: kbnIndex, + type: service.type, + id: $routeParams.id, + body: source + }) + .then(function (resp) { + return redirectHandler('updated'); + }) + .catch(notify.fatal); + }; + + function redirectHandler(action) { + return es.indices.refresh({ + index: kbnIndex + }) + .then(function (resp) { + const msg = 'You successfully ' + action + ' the "' + $scope.obj._source.title + '" ' + $scope.title.toLowerCase() + ' object'; + + $location.path('/settings/objects').search({ + _a: rison.encode({ + tab: serviceObj.title + }) }); - } + notify.info(msg); + }); } - }; - }); + } + }; }); diff --git a/src/plugins/kibana/public/settings/sections/objects/index.js b/src/plugins/kibana/public/settings/sections/objects/index.js index 08ed7d77155d0..df98bbf2f5430 100644 --- a/src/plugins/kibana/public/settings/sections/objects/index.js +++ b/src/plugins/kibana/public/settings/sections/objects/index.js @@ -1,16 +1,18 @@ -define(function (require) { - require('plugins/kibana/settings/sections/objects/_view'); - require('plugins/kibana/settings/sections/objects/_objects'); +import _ from 'lodash'; +import registry from 'ui/registry/settings_sections'; +import 'plugins/kibana/settings/sections/objects/_view'; +import 'plugins/kibana/settings/sections/objects/_objects'; +import 'ace'; +import 'ui/directives/confirm_click'; +import uiModules from 'ui/modules'; - require('ace'); - require('ui/directives/confirm_click'); - // add the module deps to this module - require('ui/modules').get('apps/settings'); +// add the module deps to this module +uiModules.get('apps/settings'); - return { - name: 'objects', - display: 'Objects', - url: '#/settings/objects' - }; -}); +registry.register(_.constant({ + order: 3, + name: 'objects', + display: 'Objects', + url: '#/settings/objects' +})); diff --git a/src/plugins/kibana/public/settings/sections/status/index.js b/src/plugins/kibana/public/settings/sections/status/index.js index 4887afcacf4cb..0b3af4c489a76 100644 --- a/src/plugins/kibana/public/settings/sections/status/index.js +++ b/src/plugins/kibana/public/settings/sections/status/index.js @@ -1,10 +1,9 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import registry from 'ui/registry/settings_sections'; - return { - order: 3, - name: 'status', - display: 'Status', - url: '/status' - }; -}); +registry.register(_.constant({ + order: 1000, + name: 'status', + display: 'Status', + url: '/status' +})); diff --git a/src/plugins/kibana/public/settings/styles/main.less b/src/plugins/kibana/public/settings/styles/main.less index ef10f7317faad..beddd122eed35 100644 --- a/src/plugins/kibana/public/settings/styles/main.less +++ b/src/plugins/kibana/public/settings/styles/main.less @@ -11,6 +11,10 @@ kbn-settings-objects-view { display: block; } +nav.navbar { + height: 70px; +} + .settings-nav { text-transform: capitalize; } @@ -19,6 +23,15 @@ li.kbn-settings-tab:first-letter { text-transform: capitalize; } +kbn-settings-app { + div.app-container { + div.container-fluid { + padding-left: 0; + padding-right: 0; + } + } +} + kbn-settings-objects { form { margin-bottom: @line-height-computed; @@ -93,6 +106,8 @@ kbn-settings-objects-view { } .advanced-settings { + overflow-x: scroll; + table { width: 100%; @@ -186,3 +201,5 @@ kbn-settings-indices { .kbn-settings-indices-create { .time-and-pattern > div {} } + +@import "~ui/dragula/gu-dragula.less"; diff --git a/src/plugins/kibana/public/visualize/editor/__tests__/agg.js b/src/plugins/kibana/public/visualize/editor/__tests__/agg.js index e2acbc9f78c22..df8d592098b7f 100644 --- a/src/plugins/kibana/public/visualize/editor/__tests__/agg.js +++ b/src/plugins/kibana/public/visualize/editor/__tests__/agg.js @@ -1,19 +1,19 @@ -var angular = require('angular'); -var $ = require('jquery'); -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import angular from 'angular'; +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import $ from 'jquery'; +import 'plugins/kibana/visualize/editor/agg'; -require('plugins/kibana/visualize/editor/agg'); describe('Vis-Editor-Agg plugin directive', function () { - var $parentScope = {}; - var $scope; - var $elem; + const $parentScope = {}; + let $scope; + let $elem; function makeConfig(which) { - var schemaMap = { + const schemaMap = { radius: { title: 'Dot Size', min: 0, @@ -25,10 +25,10 @@ describe('Vis-Editor-Agg plugin directive', function () { max: Infinity } }; - var typeOptions = ['count', 'avg', 'sum', 'min', 'max', 'cardinality']; + const typeOptions = ['count', 'avg', 'sum', 'min', 'max', 'cardinality']; which = which || 'metric'; - var schema = schemaMap[which]; + const schema = schemaMap[which]; return { min: schema.min, diff --git a/src/plugins/kibana/public/visualize/editor/__tests__/draggable.js b/src/plugins/kibana/public/visualize/editor/__tests__/draggable.js new file mode 100644 index 0000000000000..d8673d30a42e4 --- /dev/null +++ b/src/plugins/kibana/public/visualize/editor/__tests__/draggable.js @@ -0,0 +1,121 @@ +import angular from 'angular'; +import sinon from 'sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +let init; +let $rootScope; +let $compile; + +describe('draggable_* directives', function () { + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function ($injector) { + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + init = function init(markup = '') { + const $parentScope = $rootScope.$new(); + $parentScope.items = [ + { name: 'item_1' }, + { name: 'item_2' }, + { name: 'item_3' } + ]; + + // create the markup + const $elem = angular.element(`
`); + $elem.html(markup); + + // compile the directive + $compile($elem)($parentScope); + $parentScope.$apply(); + + const $scope = $elem.scope(); + + return { $parentScope, $scope, $elem }; + }; + })); + + describe('draggable_container directive', function () { + it('should expose the drake', function () { + const { $scope } = init(); + expect($scope.drake).to.be.an(Object); + }); + + it('should expose the controller', function () { + const { $scope } = init(); + expect($scope.draggableContainerCtrl).to.be.an(Object); + }); + + it('should pull item list from directive attribute', function () { + const { $scope, $parentScope } = init(); + expect($scope.draggableContainerCtrl.getList()).to.eql($parentScope.items); + }); + + it('should not be able to move extraneous DOM elements', function () { + const bare = angular.element(`
`); + const { $scope } = init(); + expect($scope.drake.canMove(bare[0])).to.eql(false); + }); + + it('should not be able to move non-[draggable-item] elements', function () { + const bare = angular.element(`
`); + const { $scope, $elem } = init(); + $elem.append(bare); + expect($scope.drake.canMove(bare[0])).to.eql(false); + }); + + it('shouldn\'t be able to move extraneous [draggable-item] elements', function () { + const anotherParent = angular.element(`
`); + const item = angular.element(`
`); + const scope = $rootScope.$new(); + anotherParent.append(item); + $compile(anotherParent)(scope); + $compile(item)(scope); + scope.$apply(); + const { $scope } = init(); + expect($scope.drake.canMove(item[0])).to.eql(false); + }); + + it('shouldn\'t be able to move [draggable-item] if it has a handle', function () { + const { $scope, $elem } = init(` +
+
+
+ `); + const item = $elem.find(`[draggable-item]`); + expect($scope.drake.canMove(item[0])).to.eql(false); + }); + + it('should be able to move [draggable-item] by its handle', function () { + const { $scope, $elem } = init(` +
+
+
+ `); + const handle = $elem.find(`[draggable-handle]`); + expect($scope.drake.canMove(handle[0])).to.eql(true); + }); + }); + + describe('draggable_item', function () { + it('should be required to be a child to [draggable-container]', function () { + const item = angular.element(`
`); + const scope = $rootScope.$new(); + expect(() => { + $compile(item)(scope); + scope.$apply(); + }).to.throwException(/controller(.+)draggableContainer(.+)required/i); + }); + }); + + describe('draggable_handle', function () { + it('should be required to be a child to [draggable-item]', function () { + const handle = angular.element(`
`); + const scope = $rootScope.$new(); + expect(() => { + $compile(handle)(scope); + scope.$apply(); + }).to.throwException(/controller(.+)draggableItem(.+)required/i); + }); + }); +}); diff --git a/src/plugins/kibana/public/visualize/editor/agg.html b/src/plugins/kibana/public/visualize/editor/agg.html index 56b3906d6fdde..965c54cc8fcb0 100644 --- a/src/plugins/kibana/public/visualize/editor/agg.html +++ b/src/plugins/kibana/public/visualize/editor/agg.html @@ -6,7 +6,7 @@ aria-label="{{ editorOpen ? 'Close Editor' : 'Open Editor' }}" ng-click="editorOpen = !editorOpen" type="button" - class="btn btn-default btn-xs vis-editor-agg-header-toggle"> + class="btn btn-primary btn-xs vis-editor-agg-header-toggle"> @@ -27,30 +27,40 @@
- + + + + - + @@ -65,7 +75,9 @@ class="btn btn-xs btn-danger"> +
+
diff --git a/src/plugins/kibana/public/visualize/editor/agg.js b/src/plugins/kibana/public/visualize/editor/agg.js index 3f551f81647c9..217d099b90e19 100644 --- a/src/plugins/kibana/public/visualize/editor/agg.js +++ b/src/plugins/kibana/public/visualize/editor/agg.js @@ -1,92 +1,95 @@ -define(function (require) { - require('ui/modules') - .get('app/visualize') - .directive('visEditorAgg', function ($compile, $parse, $filter, Private, Notifier) { - require('plugins/kibana/visualize/editor/agg_params'); - require('plugins/kibana/visualize/editor/agg_add'); - - var _ = require('lodash'); - var $ = require('jquery'); - var aggTypes = Private(require('ui/agg_types/index')); - var advancedToggleHtml = require('plugins/kibana/visualize/editor/advanced_toggle.html'); - - var notify = new Notifier({ - location: 'visAggGroup' - }); - - return { - restrict: 'A', - template: require('plugins/kibana/visualize/editor/agg.html'), - require: 'form', - link: function ($scope, $el, attrs, kbnForm) { - $scope.$bind('outputAgg', 'outputVis.aggs.byId[agg.id]', $scope); - $scope.editorOpen = !!$scope.agg.brandNew; - - $scope.$watch('editorOpen', function (open) { - // make sure that all of the form inputs are "touched" - // so that their errors propogate - if (!open) kbnForm.$setTouched(); - }); - - $scope.$watchMulti([ - '$index', - 'group.length' - ], function () { - $scope.aggIsTooLow = calcAggIsTooLow(); - }); +import 'plugins/kibana/visualize/editor/agg_params'; +import 'plugins/kibana/visualize/editor/agg_add'; +import _ from 'lodash'; +import $ from 'jquery'; +import advancedToggleHtml from 'plugins/kibana/visualize/editor/advanced_toggle.html'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import uiModules from 'ui/modules'; +import aggTemplate from 'plugins/kibana/visualize/editor/agg.html'; +uiModules +.get('app/visualize') +.directive('visEditorAgg', function ($compile, $parse, $filter, Private, Notifier) { + + const aggTypes = Private(AggTypesIndexProvider); + + const notify = new Notifier({ + location: 'visAggGroup' + }); - /** - * Describe the aggregation, for display in the collapsed agg header - * @return {[type]} [description] - */ - $scope.describe = function () { - if (!$scope.agg.type.makeLabel) return ''; - var label = $scope.agg.type.makeLabel($scope.agg); - return label ? label : ''; - }; - - function move(below, agg) { - _.move($scope.vis.aggs, agg, below, function (otherAgg) { - return otherAgg.schema.group === agg.schema.group; - }); + return { + restrict: 'A', + template: aggTemplate, + require: 'form', + link: function ($scope, $el, attrs, kbnForm) { + $scope.editorOpen = !!$scope.agg.brandNew; + + $scope.$watch('editorOpen', function (open) { + // make sure that all of the form inputs are "touched" + // so that their errors propogate + if (!open) kbnForm.$setTouched(); + }); + + $scope.$watchMulti([ + '$index', + 'group.length' + ], function () { + $scope.aggIsTooLow = calcAggIsTooLow(); + }); + + /** + * Describe the aggregation, for display in the collapsed agg header + * @return {[type]} [description] + */ + $scope.describe = function () { + if (!$scope.agg.type.makeLabel) return ''; + const label = $scope.agg.type.makeLabel($scope.agg); + return label ? label : ''; + }; + + $scope.$on('drag-start', e => { + $scope.editorWasOpen = $scope.editorOpen; + $scope.editorOpen = false; + $scope.$emit('agg-drag-start', $scope.agg); + }); + + $scope.$on('drag-end', e => { + $scope.editorOpen = $scope.editorWasOpen; + $scope.$emit('agg-drag-end', $scope.agg); + }); + + $scope.remove = function (agg) { + const aggs = $scope.vis.aggs; + + const index = aggs.indexOf(agg); + if (index === -1) return notify.log('already removed'); + + aggs.splice(index, 1); + }; + + $scope.canRemove = function (aggregation) { + const metricCount = _.reduce($scope.group, function (count, agg) { + return (agg.schema.name === aggregation.schema.name) ? ++count : count; + }, 0); + + // make sure the the number of these aggs is above the min + return metricCount > aggregation.schema.min; + }; + + function calcAggIsTooLow() { + if (!$scope.agg.schema.mustBeFirst) { + return false; } - $scope.moveUp = _.partial(move, false); - $scope.moveDown = _.partial(move, true); - - $scope.remove = function (agg) { - var aggs = $scope.vis.aggs; - - var index = aggs.indexOf(agg); - if (index === -1) return notify.log('already removed'); - - aggs.splice(index, 1); - }; - $scope.canRemove = function (aggregation) { - var metricCount = _.reduce($scope.group, function (count, agg) { - return (agg.schema.name === aggregation.schema.name) ? ++count : count; - }, 0); - - // make sure the the number of these aggs is above the min - return metricCount > aggregation.schema.min; - }; - - function calcAggIsTooLow() { - if (!$scope.agg.schema.mustBeFirst) { - return false; - } - - var firstDifferentSchema = _.findIndex($scope.group, function (agg) { - return agg.schema !== $scope.agg.schema; - }); - - if (firstDifferentSchema === -1) { - return false; - } + const firstDifferentSchema = _.findIndex($scope.group, function (agg) { + return agg.schema !== $scope.agg.schema; + }); - return $scope.$index > firstDifferentSchema; + if (firstDifferentSchema === -1) { + return false; } + + return $scope.$index > firstDifferentSchema; } - }; - }); + } + }; }); diff --git a/src/plugins/kibana/public/visualize/editor/agg_add.html b/src/plugins/kibana/public/visualize/editor/agg_add.html index c597ed8c93035..0ce5acbfb9fa5 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_add.html +++ b/src/plugins/kibana/public/visualize/editor/agg_add.html @@ -2,6 +2,7 @@
  • @@ -18,14 +19,14 @@ class="vis-editor-agg-wide-btn">
    -
    - Add {{ groupName }} +
    + Add {{ groupName }}
    -
    - Add sub-{{ groupName }} +
    + Add sub-{{ groupName }}
    -
    +
    Cancel
    -
    \ No newline at end of file +
    diff --git a/src/plugins/kibana/public/visualize/editor/agg_add.js b/src/plugins/kibana/public/visualize/editor/agg_add.js index 932e4cb55a1e0..44a3334169bda 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_add.js +++ b/src/plugins/kibana/public/visualize/editor/agg_add.js @@ -1,28 +1,30 @@ -define(function (require) { - require('ui/modules') - .get('kibana') - .directive('visEditorAggAdd', function (Private) { - var AggConfig = Private(require('ui/Vis/AggConfig')); +import VisAggConfigProvider from 'ui/vis/agg_config'; +import uiModules from 'ui/modules'; +import aggAddTemplate from 'plugins/kibana/visualize/editor/agg_add.html'; - return { - restrict: 'E', - template: require('plugins/kibana/visualize/editor/agg_add.html'), - controllerAs: 'add', - controller: function ($scope) { - var self = this; +uiModules +.get('kibana') +.directive('visEditorAggAdd', function (Private) { + const AggConfig = Private(VisAggConfigProvider); + return { + restrict: 'E', + template: aggAddTemplate, + controllerAs: 'add', + controller: function ($scope) { + const self = this; + + self.form = false; + self.submit = function (schema) { self.form = false; - self.submit = function (schema) { - self.form = false; - var aggConfig = new AggConfig($scope.vis, { - schema: schema - }); - aggConfig.brandNew = true; + const aggConfig = new AggConfig($scope.vis, { + schema: schema + }); + aggConfig.brandNew = true; - $scope.vis.aggs.push(aggConfig); - }; - } - }; - }); + $scope.vis.aggs.push(aggConfig); + }; + } + }; }); diff --git a/src/plugins/kibana/public/visualize/editor/agg_filter.js b/src/plugins/kibana/public/visualize/editor/agg_filter.js index 8f1cabb7e1983..f127238c72d25 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_filter.js +++ b/src/plugins/kibana/public/visualize/editor/agg_filter.js @@ -1,10 +1,9 @@ -define(function (require) { - var _ = require('lodash'); - var propFilter = require('ui/filters/_prop_filter'); +import _ from 'lodash'; +import propFilter from 'ui/filters/_prop_filter'; +import uiModules from 'ui/modules'; - require('ui/modules') - .get('kibana') - .filter('aggFilter', function () { - return propFilter('name'); - }); +uiModules +.get('kibana') +.filter('aggFilter', function () { + return propFilter('name'); }); diff --git a/src/plugins/kibana/public/visualize/editor/agg_group.html b/src/plugins/kibana/public/visualize/editor/agg_group.html index a9cc68ce5d108..98b12162d7c59 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_group.html +++ b/src/plugins/kibana/public/visualize/editor/agg_group.html @@ -3,16 +3,9 @@ {{ groupName }}
    -
    +
    -
    - - - +
    diff --git a/src/plugins/kibana/public/visualize/editor/agg_group.js b/src/plugins/kibana/public/visualize/editor/agg_group.js index 2aa7383be7c2b..6bc2341d7df1d 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_group.js +++ b/src/plugins/kibana/public/visualize/editor/agg_group.js @@ -1,46 +1,50 @@ -var _ = require('lodash'); - -define(function (require) { - require('ui/modules') - .get('app/visualize') - .directive('visEditorAggGroup', function (Private) { - require('plugins/kibana/visualize/editor/agg'); - require('plugins/kibana/visualize/editor/agg_add'); - require('plugins/kibana/visualize/editor/nesting_indicator'); - - return { - restrict: 'E', - template: require('plugins/kibana/visualize/editor/agg_group.html'), - scope: true, - link: function ($scope, $el, attr) { - $scope.groupName = attr.groupName; - $scope.$bind('group', 'vis.aggs.bySchemaGroup["' + $scope.groupName + '"]'); - $scope.$bind('schemas', 'vis.type.schemas["' + $scope.groupName + '"]'); - - $scope.$watchMulti([ - 'schemas', - '[]group' - ], function () { - var stats = $scope.stats = { - min: 0, - max: 0, - count: $scope.group ? $scope.group.length : 0 - }; - - if (!$scope.schemas) return; - - $scope.schemas.forEach(function (schema) { - stats.min += schema.min; - stats.max += schema.max; - }); - - $scope.availableSchema = $scope.schemas.filter(function (schema) { - var count = _.where($scope.group, { schema }).length; - if (count < schema.max) return true; - }); +import _ from 'lodash'; +import 'plugins/kibana/visualize/editor/agg'; +import 'plugins/kibana/visualize/editor/agg_add'; +import 'plugins/kibana/visualize/editor/nesting_indicator'; +import uiModules from 'ui/modules'; +import aggGroupTemplate from 'plugins/kibana/visualize/editor/agg_group.html'; + +uiModules +.get('app/visualize') +.directive('visEditorAggGroup', function (Private) { + + return { + restrict: 'E', + template: aggGroupTemplate, + scope: true, + link: function ($scope, $el, attr) { + $scope.groupName = attr.groupName; + $scope.$bind('group', 'vis.aggs.bySchemaGroup["' + $scope.groupName + '"]'); + $scope.$bind('schemas', 'vis.type.schemas["' + $scope.groupName + '"]'); + + $scope.$watchMulti([ + 'schemas', + '[]group' + ], function () { + const stats = $scope.stats = { + min: 0, + max: 0, + count: $scope.group ? $scope.group.length : 0 + }; + + if (!$scope.schemas) return; + + $scope.schemas.forEach(function (schema) { + stats.min += schema.min; + stats.max += schema.max; + stats.deprecate = schema.deprecate; + }); + + $scope.availableSchema = $scope.schemas.filter(function (schema) { + const count = _.where($scope.group, { schema }).length; + if (count < schema.max) return true; }); - } - }; + }); + + $scope.$on('agg-drag-start', e => $scope.dragging = true); + $scope.$on('agg-drag-end', e => $scope.dragging = false); + } + }; - }); }); diff --git a/src/plugins/kibana/public/visualize/editor/agg_param.js b/src/plugins/kibana/public/visualize/editor/agg_param.js index dd2986ed8a071..89e7c5b3f08c1 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_param.js +++ b/src/plugins/kibana/public/visualize/editor/agg_param.js @@ -1,31 +1,30 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import uiModules from 'ui/modules'; - require('ui/modules') - .get('app/visualize') - .directive('visAggParamEditor', function (config, $parse, Private) { - return { - restrict: 'E', - scope: true, - template: function ($el) { - return $el.html(); +uiModules +.get('app/visualize') +.directive('visAggParamEditor', function (config, $parse, Private) { + return { + restrict: 'E', + scope: true, + template: function ($el) { + return $el.html(); + }, + link: { + pre: function ($scope, $el, attr) { + $scope.$bind('aggParam', attr.aggParam); }, - link: { - pre: function ($scope, $el, attr) { - $scope.$bind('aggParam', attr.aggParam); - }, - post: function ($scope, $el, attr) { - $scope.config = config; + post: function ($scope, $el, attr) { + $scope.config = config; - $scope.optionEnabled = function (option) { - if (option && _.isFunction(option.enabled)) { - return option.enabled($scope.agg); - } + $scope.optionEnabled = function (option) { + if (option && _.isFunction(option.enabled)) { + return option.enabled($scope.agg); + } - return true; - }; - } + return true; + }; } - }; - }); + } + }; }); diff --git a/src/plugins/kibana/public/visualize/editor/agg_params.html b/src/plugins/kibana/public/visualize/editor/agg_params.html index e4473e73a8b2e..5f822ada4f0e4 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_params.html +++ b/src/plugins/kibana/public/visualize/editor/agg_params.html @@ -10,4 +10,13 @@ style="display: none;">
    - \ No newline at end of file +
    +

    + {{ agg.schema.deprecateMessage }} +

    +

    + "{{ agg.schema.title }}" has been deprecated. +

    +
    + + diff --git a/src/plugins/kibana/public/visualize/editor/agg_params.js b/src/plugins/kibana/public/visualize/editor/agg_params.js index b64599f5d1716..a525057c017cd 100644 --- a/src/plugins/kibana/public/visualize/editor/agg_params.js +++ b/src/plugins/kibana/public/visualize/editor/agg_params.js @@ -1,165 +1,165 @@ -define(function (require) { - var IndexedArray = require('ui/IndexedArray'); - - require('ui/modules') - .get('app/visualize') - .directive('visEditorAggParams', function ($compile, $parse, Private, Notifier, $filter) { - var _ = require('lodash'); - var $ = require('jquery'); - var aggTypes = Private(require('ui/agg_types/index')); - var aggSelectHtml = require('plugins/kibana/visualize/editor/agg_select.html'); - var advancedToggleHtml = require('plugins/kibana/visualize/editor/advanced_toggle.html'); - require('ui/filters/match_any'); - require('plugins/kibana/visualize/editor/agg_param'); - - var notify = new Notifier({ - location: 'visAggGroup' - }); - - return { - restrict: 'E', - template: require('plugins/kibana/visualize/editor/agg_params.html'), - scope: true, - link: function ($scope, $el, attr) { - $scope.$bind('agg', attr.agg); - $scope.$bind('groupName', attr.groupName); - - $scope.aggTypeOptions = aggTypes.byType[$scope.groupName]; - $scope.advancedToggled = false; - - // this will contain the controls for the schema (rows or columns?), which are unrelated to - // controls for the agg, which is why they are first - var $schemaEditor = $('
    ').addClass('schemaEditors').appendTo($el); - - if ($scope.agg.schema.editor) { - $schemaEditor.append($scope.agg.schema.editor); - $compile($schemaEditor)($scope.$new()); - } +import IndexedArray from 'ui/indexed_array'; +import _ from 'lodash'; +import $ from 'jquery'; +import aggSelectHtml from 'plugins/kibana/visualize/editor/agg_select.html'; +import advancedToggleHtml from 'plugins/kibana/visualize/editor/advanced_toggle.html'; +import 'ui/filters/match_any'; +import 'plugins/kibana/visualize/editor/agg_param'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import uiModules from 'ui/modules'; +import aggParamsTemplate from 'plugins/kibana/visualize/editor/agg_params.html'; + +uiModules +.get('app/visualize') +.directive('visEditorAggParams', function ($compile, $parse, Private, Notifier, $filter) { + const aggTypes = Private(AggTypesIndexProvider); + + const notify = new Notifier({ + location: 'visAggGroup' + }); - // allow selection of an aggregation - var $aggSelect = $(aggSelectHtml).appendTo($el); - $compile($aggSelect)($scope); - - // params for the selected agg, these are rebuilt every time the agg in $aggSelect changes - var $aggParamEditors; // container for agg type param editors - var $aggParamEditorsScope; - $scope.$watch('agg.type', function updateAggParamEditor(newType, oldType) { - if ($aggParamEditors) { - $aggParamEditors.remove(); - $aggParamEditors = null; - } + return { + restrict: 'E', + template: aggParamsTemplate, + scope: true, + link: function ($scope, $el, attr) { + $scope.$bind('agg', attr.agg); + $scope.$bind('groupName', attr.groupName); - // if there's an old scope, destroy it - if ($aggParamEditorsScope) { - $aggParamEditorsScope.$destroy(); - $aggParamEditorsScope = null; - } + $scope.aggTypeOptions = aggTypes.byType[$scope.groupName]; + $scope.advancedToggled = false; - // create child scope, used in the editors - $aggParamEditorsScope = $scope.$new(); + // this will contain the controls for the schema (rows or columns?), which are unrelated to + // controls for the agg, which is why they are first + const $schemaEditor = $('
    ').addClass('schemaEditors').appendTo($el); - var agg = $scope.agg; - if (!agg) return; + if ($scope.agg.schema.editor) { + $schemaEditor.append($scope.agg.schema.editor); + $compile($schemaEditor)($scope.$new()); + } - var type = $scope.agg.type; + // allow selection of an aggregation + const $aggSelect = $(aggSelectHtml).appendTo($el); + $compile($aggSelect)($scope); + + // params for the selected agg, these are rebuilt every time the agg in $aggSelect changes + let $aggParamEditors; // container for agg type param editors + let $aggParamEditorsScope; + $scope.$watch('agg.type', function updateAggParamEditor(newType, oldType) { + if ($aggParamEditors) { + $aggParamEditors.remove(); + $aggParamEditors = null; + } - if (newType !== oldType) { - // don't reset on initial load, the - // saved params should persist - agg.resetParams(); - } + // if there's an old scope, destroy it + if ($aggParamEditorsScope) { + $aggParamEditorsScope.$destroy(); + $aggParamEditorsScope = null; + } - if (!type) return; + // create child scope, used in the editors + $aggParamEditorsScope = $scope.$new(); - var aggParamHTML = { - basic: [], - advanced: [] - }; + const agg = $scope.agg; + if (!agg) return; - // build collection of agg params html - type.params.forEach(function (param, i) { - var aggParam; - // if field param exists, compute allowed fields - if (param.name === 'field') { - $aggParamEditorsScope.indexedFields = getIndexedFields(param); - } + const type = $scope.agg.type; - if ($aggParamEditorsScope.indexedFields) { - var hasIndexedFields = $aggParamEditorsScope.indexedFields.length > 0; - var isExtraParam = i > 0; - if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if their are no indexed fields. - return; - } - } + if (newType !== oldType) { + // don't reset on initial load, the + // saved params should persist + agg.resetParams(); + } + + if (!type) return; + const aggParamHTML = { + basic: [], + advanced: [] + }; - var type = 'basic'; - if (param.advanced) type = 'advanced'; + // 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 (aggParam = getAggParamHTML(param, i)) { - aggParamHTML[type].push(aggParam); + if ($aggParamEditorsScope.indexedFields) { + const hasIndexedFields = $aggParamEditorsScope.indexedFields.length > 0; + const isExtraParam = i > 0; + if (!hasIndexedFields && isExtraParam) { // don't draw the rest of the options if their are no indexed fields. + return; } + } - }); - // compile the paramEditors html elements - var paramEditors = aggParamHTML.basic; + let type = 'basic'; + if (param.advanced) type = 'advanced'; - if (aggParamHTML.advanced.length) { - paramEditors.push($(advancedToggleHtml).get(0)); - paramEditors = paramEditors.concat(aggParamHTML.advanced); + if (aggParam = getAggParamHTML(param, i)) { + aggParamHTML[type].push(aggParam); } - $aggParamEditors = $(paramEditors).appendTo($el); - $compile($aggParamEditors)($aggParamEditorsScope); }); - // build HTML editor given an aggParam and index - function getAggParamHTML(param, idx) { - // don't show params without an editor - if (!param.editor) { - return; - } + // compile the paramEditors html elements + let paramEditors = aggParamHTML.basic; - var attrs = { - 'agg-param': 'agg.type.params[' + idx + ']' - }; + if (aggParamHTML.advanced.length) { + paramEditors.push($(advancedToggleHtml).get(0)); + paramEditors = paramEditors.concat(aggParamHTML.advanced); + } - if (param.advanced) { - attrs['ng-show'] = 'advancedToggled'; - } + $aggParamEditors = $(paramEditors).appendTo($el); + $compile($aggParamEditors)($aggParamEditorsScope); + }); - return $('') - .attr(attrs) - .append(param.editor) - .get(0); + // build HTML editor given an aggParam and index + function getAggParamHTML(param, idx) { + // don't show params without an editor + if (!param.editor) { + return; } - function getIndexedFields(param) { - var fields = $scope.agg.vis.indexPattern.fields.raw; - var fieldTypes = param.filterFieldTypes; + const attrs = { + 'agg-param': 'agg.type.params[' + idx + ']' + }; - if (fieldTypes) { - fields = $filter('fieldType')(fields, fieldTypes); - fields = $filter('filter')(fields, { bucketable: true }); - fields = $filter('orderBy')(fields, ['type', 'name']); - } + if (param.advanced) { + attrs['ng-show'] = 'advancedToggled'; + } - return new IndexedArray({ + return $('') + .attr(attrs) + .append(param.editor) + .get(0); + } - /** - * @type {Array} - */ - index: ['name'], + function getIndexedFields(param) { + let fields = $scope.agg.vis.indexPattern.fields.raw; + const fieldTypes = param.filterFieldTypes; - /** - * [group description] - * @type {Array} - */ - initialSet: fields - }); + 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/plugins/kibana/public/visualize/editor/draggable_container.js b/src/plugins/kibana/public/visualize/editor/draggable_container.js new file mode 100644 index 0000000000000..c4d7ba76ea2b8 --- /dev/null +++ b/src/plugins/kibana/public/visualize/editor/draggable_container.js @@ -0,0 +1,88 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import dragula from 'dragula'; +import uiModules from 'ui/modules'; + +uiModules +.get('app/visualize') +.directive('draggableContainer', function () { + + return { + restrict: 'A', + scope: true, + controllerAs: 'draggableContainerCtrl', + controller($scope, $attrs, $parse) { + this.getList = () => $parse($attrs.draggableContainer)($scope); + }, + link($scope, $el, attr) { + const drake = dragula({ + containers: $el.toArray(), + moves(el, source, handle) { + const itemScope = $(el).scope(); + if (!('draggableItemCtrl' in itemScope)) { + return; // only [draggable-item] is draggable + } + return itemScope.draggableItemCtrl.moves(handle); + } + }); + + const drakeEvents = [ + 'cancel', + 'cloned', + 'drag', + 'dragend', + 'drop', + 'out', + 'over', + 'remove', + 'shadow' + ]; + const prettifiedDrakeEvents = { + drag: 'start', + dragend: 'end' + }; + + drakeEvents.forEach(type => { + drake.on(type, (el, ...args) => forwardEvent(type, el, ...args)); + }); + drake.on('drag', markDragging(true)); + drake.on('dragend', markDragging(false)); + drake.on('drop', drop); + $scope.$on('$destroy', drake.destroy); + $scope.drake = drake; + + function markDragging(isDragging) { + return el => { + const scope = $(el).scope(); + scope.isDragging = isDragging; + scope.$apply(); + }; + } + + function forwardEvent(type, el, ...args) { + const name = `drag-${prettifiedDrakeEvents[type] || type}`; + const scope = $(el).scope(); + scope.$broadcast(name, el, ...args); + } + + function drop(el, target, source, sibling) { + const list = $scope.draggableContainerCtrl.getList(); + const itemScope = $(el).scope(); + const item = itemScope.draggableItemCtrl.getItem(); + const toIndex = getSiblingItemIndex(list, sibling); + _.move(list, item, toIndex); + } + + function getSiblingItemIndex(list, sibling) { + if (!sibling) { // means the item was dropped at the end of the list + return list.length - 1; + } + const siblingScope = $(sibling).scope(); + const siblingItem = siblingScope.draggableItemCtrl.getItem(); + const siblingIndex = list.indexOf(siblingItem); + return siblingIndex; + } + } + }; + +}); diff --git a/src/plugins/kibana/public/visualize/editor/draggable_handle.js b/src/plugins/kibana/public/visualize/editor/draggable_handle.js new file mode 100644 index 0000000000000..7e62160b8e91a --- /dev/null +++ b/src/plugins/kibana/public/visualize/editor/draggable_handle.js @@ -0,0 +1,14 @@ +import uiModules from 'ui/modules'; + +uiModules +.get('app/visualize') +.directive('draggableHandle', function () { + return { + restrict: 'A', + require: '^draggableItem', + link($scope, $el, attr, ctrl) { + ctrl.registerHandle($el); + $el.addClass('gu-handle'); + } + }; +}); diff --git a/src/plugins/kibana/public/visualize/editor/draggable_item.js b/src/plugins/kibana/public/visualize/editor/draggable_item.js new file mode 100644 index 0000000000000..e949caf5a89fd --- /dev/null +++ b/src/plugins/kibana/public/visualize/editor/draggable_item.js @@ -0,0 +1,29 @@ +import $ from 'jquery'; +import uiModules from 'ui/modules'; + +uiModules +.get('app/visualize') +.directive('draggableItem', function () { + return { + restrict: 'A', + require: '^draggableContainer', + scope: true, + controllerAs: 'draggableItemCtrl', + controller($scope, $attrs, $parse) { + const dragHandles = $(); + + this.getItem = () => $parse($attrs.draggableItem)($scope); + this.registerHandle = $el => { + dragHandles.push(...$el); + }; + this.moves = handle => { + const $handle = $(handle); + const $anywhereInParentChain = $handle.parents().addBack(); + const movable = dragHandles.is($anywhereInParentChain); + return movable; + }; + }, + link($scope, $el, attr) { + } + }; +}); diff --git a/src/plugins/kibana/public/visualize/editor/editor.html b/src/plugins/kibana/public/visualize/editor/editor.html index 4b886f9fdfc08..58b0b08f5e73f 100644 --- a/src/plugins/kibana/public/visualize/editor/editor.html +++ b/src/plugins/kibana/public/visualize/editor/editor.html @@ -1,6 +1,13 @@ -
    +
    - + +
    + + + +
    +
    +
    - -
    - - - - - - - - - - - - - - - - - - - - - -
    +
    - - -
    @@ -131,14 +82,13 @@
    -
    -
    - - -
    -
    - - + +
    diff --git a/src/plugins/kibana/public/visualize/editor/editor.js b/src/plugins/kibana/public/visualize/editor/editor.js index d37eb924345aa..1d115af6c8eee 100644 --- a/src/plugins/kibana/public/visualize/editor/editor.js +++ b/src/plugins/kibana/public/visualize/editor/editor.js @@ -1,292 +1,309 @@ -define(function (require) { - var _ = require('lodash'); - require('plugins/kibana/visualize/saved_visualizations/saved_visualizations'); - require('plugins/kibana/visualize/editor/sidebar'); - require('plugins/kibana/visualize/editor/agg_filter'); - - require('ui/visualize'); - require('ui/collapsible_sidebar'); - - require('ui/routes') - .when('/visualize/create', { - template: require('plugins/kibana/visualize/editor/editor.html'), - resolve: { - savedVis: function (savedVisualizations, courier, $route, Private) { - var visTypes = Private(require('ui/registry/vis_types')); - var visType = _.find(visTypes, {name: $route.current.params.type}); - if (visType.requiresSearch && !$route.current.params.indexPattern && !$route.current.params.savedSearchId) { - throw new Error('You must provide either an indexPattern or a savedSearchId'); - } - - return savedVisualizations.get($route.current.params) - .catch(courier.redirectWhenMissing({ - '*': '/visualize' - })); +import _ from 'lodash'; +import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; +import 'plugins/kibana/visualize/editor/sidebar'; +import 'plugins/kibana/visualize/editor/agg_filter'; +import 'ui/navbar_extensions'; +import 'ui/visualize'; +import 'ui/collapsible_sidebar'; +import 'ui/share'; +import angular from 'angular'; +import Notifier from 'ui/notify/notifier'; +import RegistryVisTypesProvider from 'ui/registry/vis_types'; +import DocTitleProvider from 'ui/doc_title'; +import UtilsBrushEventProvider from 'ui/utils/brush_event'; +import FilterBarQueryFilterProvider from 'ui/filter_bar/query_filter'; +import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler'; +import uiRoutes from 'ui/routes'; +import uiModules from 'ui/modules'; +import editorTemplate from 'plugins/kibana/visualize/editor/editor.html'; + + +uiRoutes +.when('/visualize/create', { + template: editorTemplate, + resolve: { + savedVis: function (savedVisualizations, courier, $route, Private) { + const visTypes = Private(RegistryVisTypesProvider); + const visType = _.find(visTypes, {name: $route.current.params.type}); + if (visType.requiresSearch && !$route.current.params.indexPattern && !$route.current.params.savedSearchId) { + throw new Error('You must provide either an indexPattern or a savedSearchId'); } + + return savedVisualizations.get($route.current.params) + .catch(courier.redirectWhenMissing({ + '*': '/visualize' + })); } - }) - .when('/visualize/edit/:id', { - template: require('plugins/kibana/visualize/editor/editor.html'), - resolve: { - savedVis: function (savedVisualizations, courier, $route) { - return savedVisualizations.get($route.current.params.id) - .catch(courier.redirectWhenMissing({ - 'visualization': '/visualize', - 'search': '/settings/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern': '/settings/objects/savedVisualizations/' + $route.current.params.id, - 'index-pattern-field': '/settings/objects/savedVisualizations/' + $route.current.params.id - })); - } + } +}) +.when('/visualize/edit/:id', { + template: editorTemplate, + resolve: { + savedVis: function (savedVisualizations, courier, $route) { + return savedVisualizations.get($route.current.params.id) + .catch(courier.redirectWhenMissing({ + 'visualization': '/visualize', + 'search': '/settings/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern': '/settings/objects/savedVisualizations/' + $route.current.params.id, + 'index-pattern-field': '/settings/objects/savedVisualizations/' + $route.current.params.id + })); } - }); + } +}); - require('ui/modules') - .get('app/visualize', [ - 'kibana/notify', - 'kibana/courier' - ]) - .controller('VisEditor', function ($scope, $route, timefilter, AppState, $location, kbnUrl, $timeout, courier, Private, Promise) { - - var angular = require('angular'); - var ConfigTemplate = require('ui/ConfigTemplate'); - var Notifier = require('ui/notify/Notifier'); - var docTitle = Private(require('ui/doc_title')); - var brushEvent = Private(require('ui/utils/brush_event')); - var queryFilter = Private(require('ui/filter_bar/query_filter')); - var filterBarClickHandler = Private(require('ui/filter_bar/filter_bar_click_handler')); - - var notify = new Notifier({ - location: 'Visualization Editor' - }); +uiModules +.get('app/visualize', [ + 'kibana/notify', + 'kibana/courier' +]) +.controller('VisEditor', function ($scope, $route, timefilter, AppState, $location, kbnUrl, $timeout, courier, Private, Promise) { - var savedVis = $route.current.locals.savedVis; + const docTitle = Private(DocTitleProvider); + const brushEvent = Private(UtilsBrushEventProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); + const filterBarClickHandler = Private(FilterBarFilterBarClickHandlerProvider); - var vis = savedVis.vis; - var editableVis = vis.createEditableVis(); - vis.requesting = function () { - var requesting = editableVis.requesting; - requesting.call(vis); - requesting.call(editableVis); + const notify = new Notifier({ + location: 'Visualization Editor' + }); + + const savedVis = $route.current.locals.savedVis; + + const vis = savedVis.vis; + const editableVis = vis.createEditableVis(); + vis.requesting = function () { + const requesting = editableVis.requesting; + requesting.call(vis); + requesting.call(editableVis); + }; + + const searchSource = savedVis.searchSource; + + $scope.topNavMenu = [{ + key: 'new', + description: 'New Visualization', + run: function () { kbnUrl.change('/visualize', {}); } + }, { + key: 'save', + template: require('plugins/kibana/visualize/editor/panels/save.html'), + description: 'Save Visualization' + }, { + key: 'load', + template: require('plugins/kibana/visualize/editor/panels/load.html'), + description: 'Load Saved Visualization', + }, { + key: 'share', + template: require('plugins/kibana/visualize/editor/panels/share.html'), + description: 'Share Visualization' + }, { + key: 'refresh', + description: 'Refresh', + run: function () { $scope.fetch(); } + }]; + + if (savedVis.id) { + docTitle.change(savedVis.title); + } + + let $state = $scope.$state = (function initState() { + const savedVisState = vis.getState(); + const stateDefaults = { + uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, + linked: !!savedVis.savedSearchId, + query: searchSource.getOwn('query') || {query_string: {query: '*'}}, + filters: searchSource.getOwn('filter') || [], + vis: savedVisState }; - var searchSource = savedVis.searchSource; + $state = new AppState(stateDefaults); - // config panel templates - var configTemplate = new ConfigTemplate({ - save: require('plugins/kibana/visualize/editor/panels/save.html'), - load: require('plugins/kibana/visualize/editor/panels/load.html'), - share: require('plugins/kibana/visualize/editor/panels/share.html'), - }); - - if (savedVis.id) { - docTitle.change(savedVis.title); + if (!angular.equals($state.vis, savedVisState)) { + Promise.try(function () { + editableVis.setState($state.vis); + vis.setState(editableVis.getEnabledState()); + }) + .catch(courier.redirectWhenMissing({ + 'index-pattern-field': '/visualize' + })); } - var $state = $scope.$state = (function initState() { - var savedVisState = vis.getState(); - var stateDefaults = { - uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}, - linked: !!savedVis.savedSearchId, - query: searchSource.getOwn('query') || {query_string: {query: '*'}}, - filters: searchSource.getOwn('filter') || [], - vis: savedVisState - }; - - $state = new AppState(stateDefaults); - - if (!angular.equals($state.vis, savedVisState)) { - Promise.try(function () { - vis.setState($state.vis); - editableVis.setState($state.vis); - }) - .catch(courier.redirectWhenMissing({ - 'index-pattern-field': '/visualize' - })); + return $state; + }()); + + function init() { + // export some objects + $scope.savedVis = savedVis; + $scope.searchSource = searchSource; + $scope.vis = vis; + $scope.indexPattern = vis.indexPattern; + $scope.editableVis = editableVis; + $scope.state = $state; + $scope.uiState = $state.makeStateful('uiState'); + vis.setUiState($scope.uiState); + $scope.timefilter = timefilter; + $scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter'); + + editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state); + editableVis.listeners.brush = vis.listeners.brush = brushEvent; + + // track state of editable vis vs. "actual" vis + $scope.stageEditableVis = transferVisState(editableVis, vis, true); + $scope.resetEditableVis = transferVisState(vis, editableVis); + $scope.$watch(function () { + return editableVis.getEnabledState(); + }, function (newState) { + editableVis.dirty = !angular.equals(newState, vis.getEnabledState()); + + $scope.responseValueAggs = null; + try { + $scope.responseValueAggs = editableVis.aggs.getResponseAggs().filter(function (agg) { + return _.get(agg, 'schema.group') === 'metrics'; + }); } + // this can fail when the agg.type is changed but the + // params have not been set yet. watcher will trigger again + // when the params update + catch (e) {} // eslint-disable-line no-empty + }, true); - return $state; - }()); - - function init() { - // export some objects - $scope.savedVis = savedVis; - $scope.searchSource = searchSource; - $scope.vis = vis; - $scope.indexPattern = vis.indexPattern; - $scope.editableVis = editableVis; - $scope.state = $state; - $scope.uiState = $state.makeStateful('uiState'); - - $scope.conf = _.pick($scope, 'doSave', 'savedVis', 'shareData'); - $scope.configTemplate = configTemplate; - - editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state); - editableVis.listeners.brush = vis.listeners.brush = brushEvent; - - // track state of editable vis vs. "actual" vis - $scope.stageEditableVis = transferVisState(editableVis, vis, true); - $scope.resetEditableVis = transferVisState(vis, editableVis); - $scope.$watch(function () { - return editableVis.getState(); - }, function (newState) { - editableVis.dirty = !angular.equals(newState, vis.getState()); - - $scope.responseValueAggs = null; - try { - $scope.responseValueAggs = editableVis.aggs.getResponseAggs().filter(function (agg) { - return _.get(agg, 'schema.group') === 'metrics'; - }); - } - // this can fail when the agg.type is changed but the - // params have not been set yet. watcher will trigger again - // when the params update - catch (e) {} // eslint-disable-line no-empty - }, true); - - $state.replace(); - - $scope.$watch('searchSource.get("index").timeFieldName', function (timeField) { - timefilter.enabled = !!timeField; - }); - - // update the searchSource when filters update - $scope.$listen(queryFilter, 'update', function () { - searchSource.set('filter', queryFilter.getFilters()); - $state.save(); - }); - - // fetch data when filters fire fetch event - $scope.$listen(queryFilter, 'fetch', $scope.fetch); - - - $scope.$listen($state, 'fetch_with_changes', function (keys) { - if (_.contains(keys, 'linked') && $state.linked === true) { - // abort and reload route - $route.reload(); - return; - } - - if (_.contains(keys, 'vis')) { - $state.vis.listeners = _.defaults($state.vis.listeners || {}, vis.listeners); - - // only update when we need to, otherwise colors change and we - // risk loosing an in-progress result - vis.setState($state.vis); - editableVis.setState($state.vis); - } - - // we use state to track query, must write before we fetch - if ($state.query && !$state.linked) { - searchSource.set('query', $state.query); - } else { - searchSource.set('query', null); - } - - if (_.isEqual(keys, ['filters'])) { - // updates will happen in filter watcher if needed - return; - } - - $scope.fetch(); - }); - - // Without this manual emission, we'd miss filters and queries that were on the $state initially - $state.emit('fetch_with_changes'); - - $scope.$listen(timefilter, 'fetch', _.bindKey($scope, 'fetch')); - - $scope.$on('ready:vis', function () { - $scope.$emit('application.load'); - }); - - $scope.$on('$destroy', function () { - savedVis.destroy(); - }); - } + $state.replace(); - $scope.fetch = function () { - $state.save(); + $scope.$watch('searchSource.get("index").timeFieldName', function (timeField) { + timefilter.enabled = !!timeField; + }); + + // update the searchSource when filters update + $scope.$listen(queryFilter, 'update', function () { searchSource.set('filter', queryFilter.getFilters()); - if (!$state.linked) searchSource.set('query', $state.query); - if ($scope.vis.type.requiresSearch) { - courier.fetch(); - } - }; + $state.save(); + }); - $scope.startOver = function () { - kbnUrl.change('/visualize', {}); - }; + // fetch data when filters fire fetch event + $scope.$listen(queryFilter, 'fetch', $scope.fetch); - $scope.doSave = function () { - savedVis.id = savedVis.title; - savedVis.visState = $state.vis; - savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); - - savedVis.save() - .then(function (id) { - configTemplate.close('save'); - - if (id) { - notify.info('Saved Visualization "' + savedVis.title + '"'); - if (savedVis.id === $route.current.params.id) return; - kbnUrl.change('/visualize/edit/{{id}}', {id: savedVis.id}); - } - }, notify.fatal); - }; - $scope.shareData = function () { - return { - link: $location.absUrl(), - // This sucks, but seems like the cleanest way. Uhg. - embed: '' - }; - }; + $scope.$listen($state, 'fetch_with_changes', function (keys) { + if (_.contains(keys, 'linked') && $state.linked === true) { + // abort and reload route + $route.reload(); + return; + } - $scope.unlink = function () { - if (!$state.linked) return; + if (_.contains(keys, 'vis')) { + $state.vis.listeners = _.defaults($state.vis.listeners || {}, vis.listeners); - $state.linked = false; - var parent = searchSource.getParent(true); - var parentsParent = parent.getParent(true); + // only update when we need to, otherwise colors change and we + // risk loosing an in-progress result + vis.setState($state.vis); + editableVis.setState($state.vis); + } - // display unlinking for 2 seconds, unless it is double clicked - $scope.unlinking = $timeout($scope.clearUnlinking, 2000); + // we use state to track query, must write before we fetch + if ($state.query && !$state.linked) { + searchSource.set('query', $state.query); + } else { + searchSource.set('query', null); + } - delete savedVis.savedSearchId; - parent.set('filter', _.union(searchSource.getOwn('filter'), parent.getOwn('filter'))); + if (_.isEqual(keys, ['filters'])) { + // updates will happen in filter watcher if needed + return; + } - // copy over all state except "aggs" and filter, which is already copied - _(parent.toJSON()) - .omit('aggs') - .forOwn(function (val, key) { - searchSource.set(key, val); - }) - .commit(); + $scope.fetch(); + }); - $state.query = searchSource.get('query'); - $state.filters = searchSource.get('filter'); - searchSource.inherits(parentsParent); - }; + // Without this manual emission, we'd miss filters and queries that were on the $state initially + $state.emit('fetch_with_changes'); - $scope.clearUnlinking = function () { - if ($scope.unlinking) { - $timeout.cancel($scope.unlinking); - $scope.unlinking = null; - } - }; + $scope.$listen(timefilter, 'fetch', _.bindKey($scope, 'fetch')); - function transferVisState(fromVis, toVis, fetch) { - return function () { - toVis.setState(fromVis.getState()); - editableVis.dirty = false; - $state.vis = vis.getState(); - $state.save(); + $scope.$on('ready:vis', function () { + $scope.$emit('application.load'); + }); - if (fetch) $scope.fetch(); - }; + $scope.$on('$destroy', function () { + savedVis.destroy(); + }); + } + + $scope.fetch = function () { + $state.save(); + searchSource.set('filter', queryFilter.getFilters()); + if (!$state.linked) searchSource.set('query', $state.query); + if ($scope.vis.type.requiresSearch) { + courier.fetch(); + } + }; + + $scope.startOver = function () { + kbnUrl.change('/visualize', {}); + }; + + $scope.doSave = function () { + savedVis.id = savedVis.title; + // vis.title was not bound and it's needed to reflect title into visState + $state.vis.title = savedVis.title; + savedVis.visState = $state.vis; + savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges()); + + savedVis.save() + .then(function (id) { + $scope.kbnTopNav.close('save'); + + if (id) { + notify.info('Saved Visualization "' + savedVis.title + '"'); + if (savedVis.id === $route.current.params.id) return; + kbnUrl.change('/visualize/edit/{{id}}', {id: savedVis.id}); + } + }, notify.fatal); + }; + + $scope.unlink = function () { + if (!$state.linked) return; + + $state.linked = false; + const parent = searchSource.getParent(true); + const parentsParent = parent.getParent(true); + + // display unlinking for 2 seconds, unless it is double clicked + $scope.unlinking = $timeout($scope.clearUnlinking, 2000); + + delete savedVis.savedSearchId; + parent.set('filter', _.union(searchSource.getOwn('filter'), parent.getOwn('filter'))); + + // copy over all state except "aggs" and filter, which is already copied + _(parent.toJSON()) + .omit('aggs') + .forOwn(function (val, key) { + searchSource.set(key, val); + }) + .commit(); + + $state.query = searchSource.get('query'); + $state.filters = searchSource.get('filter'); + searchSource.inherits(parentsParent); + }; + + $scope.clearUnlinking = function () { + if ($scope.unlinking) { + $timeout.cancel($scope.unlinking); + $scope.unlinking = null; } + }; + + function transferVisState(fromVis, toVis, stage) { + return function () { + const view = fromVis.getEnabledState(); + const full = fromVis.getState(); + toVis.setState(view); + editableVis.dirty = false; + $state.vis = full; + $state.save(); - init(); - }); + if (stage) $scope.fetch(); + }; + } + + init(); }); diff --git a/src/plugins/kibana/public/visualize/editor/nesting_indicator.js b/src/plugins/kibana/public/visualize/editor/nesting_indicator.js index 00cc86204898a..6187760f8265b 100644 --- a/src/plugins/kibana/public/visualize/editor/nesting_indicator.js +++ b/src/plugins/kibana/public/visualize/editor/nesting_indicator.js @@ -1,32 +1,32 @@ -define(function (require) { - require('ui/modules') - .get('kibana') - .directive('nestingIndicator', function ($rootScope, $parse, Private) { - var _ = require('lodash'); - var $ = require('jquery'); - var getColors = Private(require('ui/vislib/components/color/color_palette')); +import _ from 'lodash'; +import $ from 'jquery'; +import VislibComponentsColorColorPaletteProvider from 'ui/vislib/components/color/color_palette'; +import uiModules from 'ui/modules'; +uiModules +.get('kibana') +.directive('nestingIndicator', function ($rootScope, $parse, Private) { + const getColors = Private(VislibComponentsColorColorPaletteProvider); - return { - restrict: 'E', - scope: { - item: '=', - list: '=' - }, - link: function ($scope, $el, attr) { - $scope.$watchCollection('list', function () { - if (!$scope.list || !$scope.item) return; + return { + restrict: 'E', + scope: { + item: '=', + list: '=' + }, + link: function ($scope, $el, attr) { + $scope.$watchCollection('list', function () { + if (!$scope.list || !$scope.item) return; - var item = $scope.item; - var index = $scope.list.indexOf($scope.item); - var bars = $scope.list.slice(0, index + 1); - var colors = getColors(bars.length); + const item = $scope.item; + const index = $scope.list.indexOf($scope.item); + const bars = $scope.list.slice(0, index + 1); + const colors = getColors(bars.length); - $el.html(bars.map(function (bar, i) { - return $(document.createElement('span')) - .css('background-color', colors[i]); - })); - }); - } - }; - }); + $el.html(bars.map(function (bar, i) { + return $(document.createElement('span')) + .css('background-color', colors[i]); + })); + }); + } + }; }); diff --git a/src/plugins/kibana/public/visualize/editor/panels/load.html b/src/plugins/kibana/public/visualize/editor/panels/load.html index f67fd0952ad30..14108487dc9b9 100644 --- a/src/plugins/kibana/public/visualize/editor/panels/load.html +++ b/src/plugins/kibana/public/visualize/editor/panels/load.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/plugins/kibana/public/visualize/editor/panels/save.html b/src/plugins/kibana/public/visualize/editor/panels/save.html index fe846fc2c80d9..e0c6471ee09fa 100644 --- a/src/plugins/kibana/public/visualize/editor/panels/save.html +++ b/src/plugins/kibana/public/visualize/editor/panels/save.html @@ -1,7 +1,7 @@ -
    +
    - +
    -
    \ No newline at end of file + diff --git a/src/plugins/kibana/public/visualize/editor/panels/share.html b/src/plugins/kibana/public/visualize/editor/panels/share.html index a356060024e00..1eeaf5afa608e 100644 --- a/src/plugins/kibana/public/visualize/editor/panels/share.html +++ b/src/plugins/kibana/public/visualize/editor/panels/share.html @@ -1,22 +1,4 @@ -
    - -

    -

    - -
    {{conf.shareData().embed}}
    -
    -

    - -

    -

    - -
    {{conf.shareData().link}}
    -
    -

    - -
    \ No newline at end of file + + diff --git a/src/plugins/kibana/public/visualize/editor/sidebar.html b/src/plugins/kibana/public/visualize/editor/sidebar.html index d2de10a1d6c13..0e153d2eb4996 100644 --- a/src/plugins/kibana/public/visualize/editor/sidebar.html +++ b/src/plugins/kibana/public/visualize/editor/sidebar.html @@ -13,7 +13,7 @@
    - diff --git a/src/plugins/kibana/public/visualize/editor/sidebar.js b/src/plugins/kibana/public/visualize/editor/sidebar.js index 56671c8e0bfaa..40187ba84bed9 100644 --- a/src/plugins/kibana/public/visualize/editor/sidebar.js +++ b/src/plugins/kibana/public/visualize/editor/sidebar.js @@ -1,22 +1,26 @@ -define(function (require) { - require('ui/modules') - .get('app/visualize') - .directive('visEditorSidebar', function () { - var _ = require('lodash'); +import _ from 'lodash'; +import 'plugins/kibana/visualize/editor/agg_group'; +import 'plugins/kibana/visualize/editor/vis_options'; +import uiModules from 'ui/modules'; +import sidebarTemplate from 'plugins/kibana/visualize/editor/sidebar.html'; +uiModules +.get('app/visualize') +.directive('visEditorSidebar', function () { - require('plugins/kibana/visualize/editor/agg_group'); - require('plugins/kibana/visualize/editor/vis_options'); - return { - restrict: 'E', - template: require('plugins/kibana/visualize/editor/sidebar.html'), - scope: true, - controllerAs: 'sidebar', - controller: function ($scope) { - $scope.$bind('vis', 'editableVis'); - $scope.$bind('outputVis', 'vis'); - this.section = _.get($scope, 'vis.type.requiresSearch') ? 'data' : 'options'; - } - }; - }); + return { + restrict: 'E', + template: sidebarTemplate, + scope: true, + controllerAs: 'sidebar', + controller: function ($scope) { + $scope.$bind('vis', 'editableVis'); + $scope.$watch('vis.type', (visType) => { + if (visType) { + this.showData = visType.schemas.buckets || visType.schemas.metrics; + this.section = this.section || (this.showData ? 'data' : 'options'); + } + }); + } + }; }); diff --git a/src/plugins/kibana/public/visualize/editor/styles/_editor.less b/src/plugins/kibana/public/visualize/editor/styles/_editor.less index d7a988196a33a..b771fbaf083a3 100644 --- a/src/plugins/kibana/public/visualize/editor/styles/_editor.less +++ b/src/plugins/kibana/public/visualize/editor/styles/_editor.less @@ -6,11 +6,40 @@ @vis-editor-nesting-width: 8px; @vis-editor-agg-editor-spacing: 5px; + &-info { + line-height: 30px; + padding: 0px 10px; + border-bottom-left-radius: @border-radius-base; + + + &-title { + font-weight: bold; + margin-right: 10px; + } + } + // For the vis-editor sidebar nav .navbar-default .navbar-nav { - &> .active > a:before { - border: 7px solid transparent; - border-bottom-color: @vis-editor-navbar-current-tab-color; + &> .active > a { + border-bottom: 2px solid @kibanaGray2; + color: @kibanaGray1; + &:before { + display: none; + } + &:hover { + background-color: transparent; + } + } + + > li { + > a { + padding: 4px 0; + margin: 0 10px; + color: @kibanaGray2; + } + > a:hover { + border-bottom: 2px solid @kibanaGray2; + } } .danger { @@ -55,8 +84,10 @@ } } + &-content { .flex-parent(); + z-index: 0; // overrides for tablet and desktop @media (min-width: @screen-md-min) { @@ -78,7 +109,9 @@ max-width: @vis-editor-sidebar-min-width; } + .index-pattern, nav { + min-height: @app-icon-height; border-radius: 0px; } @@ -114,19 +147,13 @@ } .sidebar-item-title { - font-size: 20px; + font-size: 14px; font-weight: bold; border: inherit !important; - background-color: @vis-editor-sidebar-title-bg; margin-bottom: @vis-editor-agg-editor-spacing; padding: 2px 5px !important; } - .sidebar-item-title:hover { - color: @vis-editor-sidebar-title-hover-color !important; - background-color: @vis-editor-sidebar-title-hover-bg !important; - } - .hintbox { padding: @vis-editor-agg-editor-spacing; margin-bottom: @vis-editor-agg-editor-spacing; @@ -177,7 +204,7 @@ &-group { .flex-parent(0, 1, auto); - color: @vis-editor-agg-group-color; + color: @kibanaGray2; } &-header { @@ -235,16 +262,6 @@ } } - .regex .flags { - .docs { - text-align: right; - } - - a { - color: @vis-editor-agg-editor-flags-color; - } - } - &-advanced-toggle { text-align: right; } @@ -274,9 +291,7 @@ } &-wide-btn { - border-radius: 0; - border-top: 2px solid; - border-top-color: @vis-editor-agg-wide-btn-border; + text-align: center; &-add { width: 140px; @@ -355,7 +370,6 @@ } .visualize-chart { - flex: 1 1 100%; position: relative; } } diff --git a/src/plugins/kibana/public/visualize/editor/vis_options.js b/src/plugins/kibana/public/visualize/editor/vis_options.js index 762a1e3e8f699..1506d881c0cc6 100644 --- a/src/plugins/kibana/public/visualize/editor/vis_options.js +++ b/src/plugins/kibana/public/visualize/editor/vis_options.js @@ -1,25 +1,26 @@ -define(function (require) { - var _ = require('lodash'); - var $ = require('jquery'); +import _ from 'lodash'; +import $ from 'jquery'; +import uiModules from 'ui/modules'; +import visOptionsTemplate from 'plugins/kibana/visualize/editor/vis_options.html'; - require('ui/modules') - .get('app/visualize') - .directive('visEditorVisOptions', function (Private, $timeout, $compile) { - return { - restrict: 'E', - template: require('plugins/kibana/visualize/editor/vis_options.html'), - scope: { - vis: '=', - }, - link: function ($scope, $el) { - var $optionContainer = $('.visualization-options'); - var $editor = $compile($scope.vis.type.params.editor)($scope); - $optionContainer.append($editor); +uiModules +.get('app/visualize') +.directive('visEditorVisOptions', function (Private, $timeout, $compile) { + return { + restrict: 'E', + template: visOptionsTemplate, + scope: { + vis: '=', + savedVis: '=', + }, + link: function ($scope, $el) { + const $optionContainer = $el.find('.visualization-options'); + const $editor = $compile($scope.vis.type.params.editor)($scope); + $optionContainer.append($editor); - $scope.$watch('vis.type.schemas.all.length', function (len) { - $scope.alwaysShowOptions = len === 0; - }); - } - }; - }); + $scope.$watch('vis.type.schemas.all.length', function (len) { + $scope.alwaysShowOptions = len === 0; + }); + } + }; }); diff --git a/src/plugins/kibana/public/visualize/index.js b/src/plugins/kibana/public/visualize/index.js index 870549bb11c1a..88f684f59ccb9 100644 --- a/src/plugins/kibana/public/visualize/index.js +++ b/src/plugins/kibana/public/visualize/index.js @@ -1,30 +1,31 @@ -define(function (require) { - require('plugins/kibana/visualize/styles/main.less'); +import 'plugins/kibana/visualize/styles/main.less'; +import 'plugins/kibana/visualize/editor/editor'; +import 'plugins/kibana/visualize/wizard/wizard'; +import 'plugins/kibana/visualize/editor/add_bucket_agg'; +import 'plugins/kibana/visualize/editor/agg'; +import 'plugins/kibana/visualize/editor/agg_add'; +import 'plugins/kibana/visualize/editor/agg_filter'; +import 'plugins/kibana/visualize/editor/agg_group'; +import 'plugins/kibana/visualize/editor/agg_param'; +import 'plugins/kibana/visualize/editor/agg_params'; +import 'plugins/kibana/visualize/editor/nesting_indicator'; +import 'plugins/kibana/visualize/editor/sidebar'; +import 'plugins/kibana/visualize/editor/vis_options'; +import 'plugins/kibana/visualize/editor/draggable_container'; +import 'plugins/kibana/visualize/editor/draggable_item'; +import 'plugins/kibana/visualize/editor/draggable_handle'; +import 'plugins/kibana/visualize/saved_visualizations/_saved_vis'; +import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; +import uiRoutes from 'ui/routes'; - require('plugins/kibana/visualize/editor/editor'); - require('plugins/kibana/visualize/wizard/wizard'); - require('ui/routes') - .when('/visualize', { - redirectTo: '/visualize/step/1' - }); +uiRoutes +.when('/visualize', { + redirectTo: '/visualize/step/1' +}); - // preloading - require('plugins/kibana/visualize/editor/add_bucket_agg'); - require('plugins/kibana/visualize/editor/agg'); - require('plugins/kibana/visualize/editor/agg_add'); - require('plugins/kibana/visualize/editor/agg_filter'); - require('plugins/kibana/visualize/editor/agg_group'); - require('plugins/kibana/visualize/editor/agg_param'); - require('plugins/kibana/visualize/editor/agg_params'); - require('plugins/kibana/visualize/editor/editor'); - require('plugins/kibana/visualize/editor/nesting_indicator'); - require('plugins/kibana/visualize/editor/sidebar'); - require('plugins/kibana/visualize/editor/vis_options'); - require('plugins/kibana/visualize/saved_visualizations/_saved_vis'); - require('plugins/kibana/visualize/saved_visualizations/saved_visualizations'); +// preloading - require('ui/saved_objects/saved_object_registry') - .register(require('plugins/kibana/visualize/saved_visualizations/saved_visualization_register')); +require('ui/saved_objects/saved_object_registry') +.register(require('plugins/kibana/visualize/saved_visualizations/saved_visualization_register')); -}); diff --git a/src/plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index 534a8ae25346d..20ab2138469b7 100644 --- a/src/plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -1,123 +1,130 @@ -define(function (require) { - require('ui/modules') - .get('app/visualize') - .factory('SavedVis', function (config, $injector, courier, Promise, savedSearches, Private, Notifier) { - var _ = require('lodash'); - var Vis = Private(require('ui/Vis')); - - var notify = new Notifier({ - location: 'SavedVis' - }); +import _ from 'lodash'; +import VisProvider from 'ui/vis'; +import uiModules from 'ui/modules'; +uiModules +.get('app/visualize') +.factory('SavedVis', function (config, $injector, courier, Promise, savedSearches, Private, Notifier) { + const Vis = Private(VisProvider); + + const notify = new Notifier({ + location: 'SavedVis' + }); - _.class(SavedVis).inherits(courier.SavedObject); - function SavedVis(opts) { - var self = this; - opts = opts || {}; - if (typeof opts !== 'object') opts = { id: opts }; - - SavedVis.Super.call(self, { - type: SavedVis.type, - mapping: SavedVis.mapping, - searchSource: SavedVis.searchSource, - - id: opts.id, - indexPattern: opts.indexPattern, - defaults: { - title: 'New Visualization', - visState: (function () { - if (!opts.type) return null; - var def = {}; - def.type = opts.type; - return def; - }()), - uiStateJSON: '{}', - description: '', - savedSearchId: opts.savedSearchId, - version: 1 - }, - - afterESResp: this._afterEsResp + _.class(SavedVis).inherits(courier.SavedObject); + function SavedVis(opts) { + const self = this; + opts = opts || {}; + if (typeof opts !== 'object') opts = { id: opts }; + + SavedVis.Super.call(self, { + type: SavedVis.type, + mapping: SavedVis.mapping, + searchSource: SavedVis.searchSource, + + id: opts.id, + indexPattern: opts.indexPattern, + defaults: { + title: 'New Visualization', + visState: (function () { + if (!opts.type) return null; + const def = {}; + def.type = opts.type; + return def; + }()), + uiStateJSON: '{}', + description: '', + savedSearchId: opts.savedSearchId, + version: 1 + }, + + afterESResp: this._afterEsResp + }); + } + + SavedVis.type = 'visualization'; + + SavedVis.mapping = { + title: 'string', + visState: 'json', + uiStateJSON: 'string', + description: 'string', + savedSearchId: 'string', + version: 'integer' + }; + + SavedVis.searchSource = true; + + SavedVis.prototype._afterEsResp = function () { + const self = this; + + return self._getLinkedSavedSearch() + .then(function () { + self.searchSource.size(0); + + return self.vis ? self._updateVis() : self._createVis(); + }) + .then(function (vis) { + self.searchSource.aggs(function () { + self.vis.requesting(); + return self.vis.aggs.toDsl(); }); - } - SavedVis.type = 'visualization'; - - SavedVis.mapping = { - title: 'string', - visState: 'json', - uiStateJSON: 'string', - description: 'string', - savedSearchId: 'string', - version: 'integer' - }; - - SavedVis.searchSource = true; + return self; + }); + }; - SavedVis.prototype._afterEsResp = function () { - var self = this; + SavedVis.prototype._getLinkedSavedSearch = Promise.method(function () { + const self = this; + const linkedSearch = !!self.savedSearchId; + const current = self.savedSearch; - return self._getLinkedSavedSearch() - .then(function () { - self.searchSource.size(0); + if (linkedSearch && current && current.id === self.savedSearchId) { + return; + } - return self.vis ? self._updateVis() : self._createVis(); - }) - .then(function (vis) { - self.searchSource.aggs(function () { - self.vis.requesting(); - return self.vis.aggs.toDsl(); - }); + if (self.savedSearch) { + self.searchSource.inherits(self.savedSearch.searchSource.getParent()); + self.savedSearch.destroy(); + self.savedSearch = null; + } - return self; + if (linkedSearch) { + return savedSearches.get(self.savedSearchId) + .then(function (savedSearch) { + self.savedSearch = savedSearch; + self.searchSource.inherits(self.savedSearch.searchSource); }); - }; - - SavedVis.prototype._getLinkedSavedSearch = Promise.method(function () { - var self = this; - var linkedSearch = !!self.savedSearchId; - var current = self.savedSearch; - - if (linkedSearch && current && current.id === self.savedSearchId) { - return; - } - - if (self.savedSearch) { - self.searchSource.inherits(self.savedSearch.searchSource.getParent()); - self.savedSearch.destroy(); - self.savedSearch = null; - } - - if (linkedSearch) { - return savedSearches.get(self.savedSearchId) - .then(function (savedSearch) { - self.savedSearch = savedSearch; - self.searchSource.inherits(self.savedSearch.searchSource); - }); - } - }); + } + }); - SavedVis.prototype._createVis = function () { - var self = this; + SavedVis.prototype._createVis = function () { + const self = this; - if (self.stateJSON) { - self.visState = Vis.convertOldState(self.typeName, JSON.parse(self.stateJSON)); - } + if (self.stateJSON) { + self.visState = Vis.convertOldState(self.typeName, JSON.parse(self.stateJSON)); + } - self.vis = new Vis( - self.searchSource.get('index'), - self.visState - ); + // visState doesn't yet exist when importing a visualization, so we can't + // assume that exists at this point. If it does exist, then we're not + // importing a visualization, so we want to sync the title. + if (self.visState) { + self.visState.title = self.title; + } + self.vis = new Vis( + self.searchSource.get('index'), + self.visState + ); - return self.vis; - }; + return self.vis; + }; - SavedVis.prototype._updateVis = function () { - var self = this; + SavedVis.prototype._updateVis = function () { + const self = this; - self.vis.indexPattern = self.searchSource.get('index'); - self.vis.setState(self.visState); - }; + self.vis.indexPattern = self.searchSource.get('index'); + self.visState.title = self.title; + self.vis.setState(self.visState); + }; - return SavedVis; - }); + return SavedVis; }); diff --git a/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js b/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js index a6d59dda16fed..36d563ca69b08 100644 --- a/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js +++ b/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js @@ -1,5 +1,3 @@ -define(function (require) { - return function savedVisualizationFn(savedVisualizations) { - return savedVisualizations; - }; -}); +export default function savedVisualizationFn(savedVisualizations) { + return savedVisualizations; +}; diff --git a/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index 8019ff9c540a7..aa711aa0adf57 100644 --- a/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -1,95 +1,110 @@ -define(function (require) { - var app = require('ui/modules').get('app/visualize'); - var _ = require('lodash'); +import _ from 'lodash'; +import Scanner from 'ui/utils/scanner'; +import 'plugins/kibana/visualize/saved_visualizations/_saved_vis'; +import RegistryVisTypesProvider from 'ui/registry/vis_types'; +import uiModules from 'ui/modules'; +const app = uiModules.get('app/visualize'); - require('plugins/kibana/visualize/saved_visualizations/_saved_vis'); - // Register this service with the saved object registry so it can be - // edited by the object editor. - require('plugins/kibana/settings/saved_object_registry').register({ - service: 'savedVisualizations', - title: 'visualizations' +// Register this service with the saved object registry so it can be +// edited by the object editor. +require('plugins/kibana/settings/saved_object_registry').register({ + service: 'savedVisualizations', + title: 'visualizations' +}); + +app.service('savedVisualizations', function (Promise, es, kbnIndex, SavedVis, Private, Notifier, kbnUrl) { + const visTypes = Private(RegistryVisTypesProvider); + + const scanner = new Scanner(es, { + index: kbnIndex, + type: 'visualization' + }); + + const notify = new Notifier({ + location: 'Saved Visualization Service' }); - app.service('savedVisualizations', function (Promise, es, kbnIndex, SavedVis, Private, Notifier, kbnUrl) { - var visTypes = Private(require('ui/registry/vis_types')); - var notify = new Notifier({ - location: 'Saved Visualization Service' + this.type = SavedVis.type; + this.Class = SavedVis; + + this.loaderProperties = { + name: 'visualizations', + noun: 'Visualization', + nouns: 'visualizations' + }; + + this.get = function (id) { + return (new SavedVis(id)).init(); + }; + + this.urlFor = function (id) { + return kbnUrl.eval('#/visualize/edit/{{id}}', {id: id}); + }; + + this.delete = function (ids) { + ids = !_.isArray(ids) ? [ids] : ids; + return Promise.map(ids, function (id) { + return (new SavedVis(id)).delete(); }); + }; - this.type = SavedVis.type; - this.Class = SavedVis; - - this.loaderProperties = { - name: 'visualizations', - noun: 'Visualization', - nouns: 'visualizations' - }; - - this.get = function (id) { - return (new SavedVis(id)).init(); - }; - - this.urlFor = function (id) { - return kbnUrl.eval('#/visualize/edit/{{id}}', {id: id}); - }; - - this.delete = function (ids) { - ids = !_.isArray(ids) ? [ids] : ids; - return Promise.map(ids, function (id) { - return (new SavedVis(id)).delete(); - }); - }; - - this.find = function (searchString, size = 100) { - var self = this; - var body; - if (searchString) { - body = { - query: { - simple_query_string: { - query: searchString + '*', - fields: ['title^3', 'description'], - default_operator: 'AND' - } + this.scanAll = function (queryString, pageSize = 1000) { + return scanner.scanAndMap(queryString, { + pageSize, + docCount: Infinity + }, (hit) => this.mapHits(hit)); + }; + + this.mapHits = function (hit) { + const source = hit._source; + source.id = hit._id; + source.url = this.urlFor(hit._id); + + let typeName = source.typeName; + if (source.visState) { + try { typeName = JSON.parse(source.visState).type; } + catch (e) { /* missing typename handled below */ } // eslint-disable-line no-empty + } + + if (!typeName || !visTypes.byName[typeName]) { + if (!typeName) notify.error('Visualization type is missing. Please add a type to this visualization.', hit); + else notify.error('Visualization type of "' + typeName + '" is invalid. Please change to a valid type.', hit); + return kbnUrl.redirect('/settings/objects/savedVisualizations/{{id}}', {id: source.id}); + } + + source.type = visTypes.byName[typeName]; + source.icon = source.type.icon; + return source; + }; + + this.find = function (searchString, size = 100) { + let body; + if (searchString) { + body = { + query: { + simple_query_string: { + query: searchString + '*', + fields: ['title^3', 'description'], + default_operator: 'AND' } - }; - } else { - body = { query: {match_all: {}}}; - } - - return es.search({ - index: kbnIndex, - type: 'visualization', - body: body, - size: size - }) - .then(function (resp) { - return { - total: resp.hits.total, - hits: _.transform(resp.hits.hits, function (hits, hit) { - var source = hit._source; - source.id = hit._id; - source.url = self.urlFor(hit._id); - - var typeName = source.typeName; - if (source.visState) { - try { typeName = JSON.parse(source.visState).type; } - catch (e) { /* missing typename handled below */ } // eslint-disable-line no-empty - } - - if (!typeName || !visTypes.byName[typeName]) { - if (!typeName) notify.error('Visualization type is missing. Please add a type to this visualization.', hit); - else notify.error('Visualization type of "' + typeName + '" is invalid. Please change to a valid type.', hit); - return kbnUrl.redirect('/settings/objects/savedVisualizations/{{id}}', {id: source.id}); - } - - source.type = visTypes.byName[typeName]; - source.icon = source.type.icon; - hits.push(source); - }, []) - }; - }); - }; - }); + } + }; + } else { + body = { query: {match_all: {}}}; + } + + return es.search({ + index: kbnIndex, + type: 'visualization', + body: body, + size: size + }) + .then((resp) => { + return { + total: resp.hits.total, + hits: resp.hits.hits.map((hit) => this.mapHits(hit)) + }; + }); + }; }); diff --git a/src/plugins/kibana/public/visualize/styles/main.less b/src/plugins/kibana/public/visualize/styles/main.less index 1f0bd0a1b6b2e..cfbd3addd324b 100644 --- a/src/plugins/kibana/public/visualize/styles/main.less +++ b/src/plugins/kibana/public/visualize/styles/main.less @@ -3,57 +3,102 @@ @import (reference) "~ui/styles/list-group-menu"; .vis-wizard { - h1 { - margin-top: 45px; - } -} + margin-right: 0; + margin-left: 0; + padding-left: 0; + padding-right: 0; -.wizard-vis-type { - .list-group-item(); - .list-group-menu .list-group-menu-item(); + @media (min-width: @screen-lg) { + .wizard { + padding: 0; + display: flex; - // overrided for tablet and desktop - @media (min-width: @screen-md-min) { - display: flex; - align-items: flex-start; - } + div.wizard-small { + flex: 2; + } - &-heading { - flex: 0 0 200px; - display: flex; - align-items: center; - font-size: 1.2em; - - .fa { - flex: 0 0 auto; - margin-right: @padding-base-horizontal; - font-size: 1.5em; - text-align: center; - } + div.wizard-large { + flex: 3; + } + + .wizard-column { + flex: 1; + display: flex; + flex-direction: column; + padding: 0px 2.5px; - h4 { - flex: 1 0 auto; + .wizard-row { + flex: 1; + background-color: @kibanaGray6; + } + } } } - &-description { - flex: 1 1 auto; - color: @wizard-vis-type-description-color; + h3 { + margin-top: 0px; + margin-bottom: 8px; + padding: 0px 5px; } -} -.visualize-info { - align-self: flex-end; + .wizard-row { + .panel { + margin-bottom: 0; + border: none; + } + + .panel-default > .panel-heading { + background-color: @kibanaGray6; + } + + .list-group { + margin-bottom: 0; + } - &-tab { - background-color: @visualize-info-bg; - padding: 5px 10px; - margin-left: @padding-base-horizontal; - border-bottom-left-radius: @border-radius-base; - text-align: right; - font-weight: bold; + .striped { + li:nth-child(odd) { + background-color: @white; + } + + li:nth-child(even) { + background-color: @kibanaGray6; + } + } } + .wizard-type { + flex: 1; + .list-group-item(); + .list-group-menu .list-group-menu-item(); + + border: none; + border-radius: 0; + background-color: @kibanaGray6; + + &-heading { + flex: 0 0 200px; + display: flex; + align-items: center; + font-size: 1.2em; + + .fa { + flex: 0 0 auto; + margin-right: @padding-base-horizontal; + font-size: 1.5em; + text-align: center; + color: @saved-object-finder-icon-color; + } + + h4 { + flex: 1 0 auto; + } + } + + &-description { + flex: 1 1 auto; + color: @wizard-vis-type-description-color; + } + } } @import "../editor/styles/_editor.less"; diff --git a/src/plugins/kibana/public/visualize/wizard/step_1.html b/src/plugins/kibana/public/visualize/wizard/step_1.html index f435e5faeaddd..d38dd53a13ca8 100644 --- a/src/plugins/kibana/public/visualize/wizard/step_1.html +++ b/src/plugins/kibana/public/visualize/wizard/step_1.html @@ -1,24 +1,23 @@ -

    - Create a new visualization
    - - Step 1 -

    - -
    - -
    - -

    {{type.title}}

    + +
    + +
    +

    Or, Open a Saved Visualization

    + + +
    - -

    Or, open a saved visualization

    - - - diff --git a/src/plugins/kibana/public/visualize/wizard/step_2.html b/src/plugins/kibana/public/visualize/wizard/step_2.html index 4870363134207..bc49fab790d95 100644 --- a/src/plugins/kibana/public/visualize/wizard/step_2.html +++ b/src/plugins/kibana/public/visualize/wizard/step_2.html @@ -1,37 +1,22 @@ -

    - Select a search source - - Step 2 -

    -
      -
    • - From a new search -
    • -
    • - - Select an index pattern -
      - -
      -
    • - -
    • - From a saved search -
    • -
    • + +
      +
      +

      From a New Search, Select Index

      + + +
      +
      +

      Or, From a Saved Search

      + title="Saved Searches" + type="searches" + class="wizard-row" + make-url="step2WithSearchUrl"> -
    • -
    \ No newline at end of file +
    +
    diff --git a/src/plugins/kibana/public/visualize/wizard/wizard.js b/src/plugins/kibana/public/visualize/wizard/wizard.js index 3644127b4ffbf..64d71203d56bb 100644 --- a/src/plugins/kibana/public/visualize/wizard/wizard.js +++ b/src/plugins/kibana/public/visualize/wizard/wizard.js @@ -1,71 +1,64 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations'; +import 'ui/directives/saved_object_finder'; +import 'ui/directives/paginated_selectable_list'; +import 'plugins/kibana/discover/saved_searches/saved_searches'; +import routes from 'ui/routes'; +import RegistryVisTypesProvider from 'ui/registry/vis_types'; +import uiModules from 'ui/modules'; - require('plugins/kibana/visualize/saved_visualizations/saved_visualizations'); - require('ui/directives/saved_object_finder'); - require('plugins/kibana/discover/saved_searches/saved_searches'); - var templateStep = function (num, txt) { - return '
    ' + txt + '
    '; - }; +const templateStep = function (num, txt) { + return '
    ' + txt + '
    '; +}; - var module = require('ui/modules').get('app/visualize', ['kibana/courier']); - var routes = require('ui/routes'); +const module = uiModules.get('app/visualize', ['kibana/courier']); - /******** - /** Wizard Step 1 - /********/ - routes.when('/visualize/step/1', { - template: templateStep(1, require('plugins/kibana/visualize/wizard/step_1.html')) - }); +/******** +/** Wizard Step 1 +/********/ +routes.when('/visualize/step/1', { + template: templateStep(1, require('plugins/kibana/visualize/wizard/step_1.html')) +}); - module.controller('VisualizeWizardStep1', function ($scope, $route, $location, timefilter, Private) { - timefilter.enabled = false; +module.controller('VisualizeWizardStep1', function ($scope, $route, $location, timefilter, Private) { + timefilter.enabled = false; - $scope.visTypes = Private(require('ui/registry/vis_types')); - $scope.visTypeUrl = function (visType) { - if (!visType.requiresSearch) return '#/visualize/create?type=' + encodeURIComponent(visType.name); - else return '#/visualize/step/2?type=' + encodeURIComponent(visType.name); - }; - }); + $scope.visTypes = Private(RegistryVisTypesProvider); + $scope.visTypeUrl = function (visType) { + if (!visType.requiresSearch) return '#/visualize/create?type=' + encodeURIComponent(visType.name); + else return '#/visualize/step/2?type=' + encodeURIComponent(visType.name); + }; +}); - /******** - /** Wizard Step 2 - /********/ - routes.when('/visualize/step/2', { - template: templateStep(2, require('plugins/kibana/visualize/wizard/step_2.html')), - resolve: { - indexPatternIds: function (courier) { - return courier.indexPatterns.getIds(); - } +/******** +/** Wizard Step 2 +/********/ +routes.when('/visualize/step/2', { + template: templateStep(2, require('plugins/kibana/visualize/wizard/step_2.html')), + resolve: { + indexPatternIds: function (courier) { + return courier.indexPatterns.getIds(); } - }); - - module.controller('VisualizeWizardStep2', function ($route, $scope, $location, timefilter, kbnUrl) { - var type = $route.current.params.type; + } +}); - $scope.step2WithSearchUrl = function (hit) { - return kbnUrl.eval('#/visualize/create?&type={{type}}&savedSearchId={{id}}', {type: type, id: hit.id}); - }; +module.controller('VisualizeWizardStep2', function ($route, $scope, $location, timefilter, kbnUrl) { + const type = $route.current.params.type; - timefilter.enabled = false; + $scope.step2WithSearchUrl = function (hit) { + return kbnUrl.eval('#/visualize/create?&type={{type}}&savedSearchId={{id}}', {type: type, id: hit.id}); + }; - $scope.indexPattern = { - selection: null, - list: $route.current.locals.indexPatternIds - }; + timefilter.enabled = false; - $scope.$watch('stepTwoMode', function (mode) { - if (mode === 'new') { - if ($scope.indexPattern.list && $scope.indexPattern.list.length === 1) { - $scope.indexPattern.selection = $scope.indexPattern.list[0]; - } - } - }); + $scope.indexPattern = { + selection: null, + list: $route.current.locals.indexPatternIds + }; - $scope.$watch('indexPattern.selection', function (pattern) { - if (!pattern) return; - kbnUrl.change('/visualize/create?type={{type}}&indexPattern={{pattern}}', {type: type, pattern: pattern}); - }); - }); + $scope.makeUrl = function (pattern) { + if (!pattern) return; + return `#/visualize/create?type=${type}&indexPattern=${pattern}`; + }; }); diff --git a/src/plugins/kibana/server/lib/__tests__/case_conversion.js b/src/plugins/kibana/server/lib/__tests__/case_conversion.js new file mode 100644 index 0000000000000..b7894126c5a5b --- /dev/null +++ b/src/plugins/kibana/server/lib/__tests__/case_conversion.js @@ -0,0 +1,35 @@ +import expect from 'expect.js'; +import _ from 'lodash'; +import { keysToSnakeCaseShallow, keysToCamelCaseShallow } from '../case_conversion'; + +describe('keysToSnakeCaseShallow', function () { + + it('should convert all of an object\'s keys to snake case', function () { + const result = keysToSnakeCaseShallow({ + camelCase: 'camel_case', + 'kebab-case': 'kebab_case', + snake_case: 'snake_case' + }); + + _.forEach(result, function (value, key) { + expect(key).to.be(value); + }); + }); + +}); + +describe('keysToCamelCaseShallow', function () { + + it('should convert all of an object\'s keys to camel case', function () { + const result = keysToCamelCaseShallow({ + camelCase: 'camelCase', + 'kebab-case': 'kebabCase', + snake_case: 'snakeCase' + }); + + _.forEach(result, function (value, key) { + expect(key).to.be(value); + }); + }); + +}); diff --git a/src/plugins/kibana/server/lib/__tests__/convert_pattern_and_template_name.js b/src/plugins/kibana/server/lib/__tests__/convert_pattern_and_template_name.js new file mode 100644 index 0000000000000..d33775b0d8e50 --- /dev/null +++ b/src/plugins/kibana/server/lib/__tests__/convert_pattern_and_template_name.js @@ -0,0 +1,31 @@ +import expect from 'expect.js'; +import {templateToPattern, patternToTemplate} from '../convert_pattern_and_template_name'; + +describe('convertPatternAndTemplateName', function () { + + describe('templateToPattern', function () { + + it('should convert an index template\'s name to its matching index pattern\'s title', function () { + expect(templateToPattern('kibana-logstash-*')).to.be('logstash-*'); + }); + + it('should throw an error if the template name isn\'t a valid kibana namespaced name', function () { + expect(templateToPattern).withArgs('logstash-*').to.throwException('not a valid kibana namespaced template name'); + expect(templateToPattern).withArgs('').to.throwException(/not a valid kibana namespaced template name/); + }); + + }); + + describe('patternToTemplate', function () { + + it('should convert an index pattern\'s title to its matching index template\'s name', function () { + expect(patternToTemplate('logstash-*')).to.be('kibana-logstash-*'); + }); + + it('should throw an error if the pattern is empty', function () { + expect(patternToTemplate).withArgs('').to.throwException(/pattern must not be empty/); + }); + + }); + +}); diff --git a/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js b/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js new file mode 100644 index 0000000000000..7444d38384638 --- /dev/null +++ b/src/plugins/kibana/server/lib/__tests__/create_mappings_from_pattern_fields.js @@ -0,0 +1,82 @@ +import createMappingsFromPatternFields from '../create_mappings_from_pattern_fields'; +import expect from 'expect.js'; +import _ from 'lodash'; + +let testFields; + +describe('createMappingsFromPatternFields', function () { + + beforeEach(function () { + testFields = [ + { + 'name': 'ip', + 'type': 'ip' + }, + { + 'name': 'agent', + 'type': 'string' + }, + { + 'name': 'bytes', + 'type': 'number' + } + ]; + }); + + it('should throw an error if the argument is empty', function () { + expect(createMappingsFromPatternFields).to.throwException(/argument must not be empty/); + }); + + it('should not modify the original argument', function () { + const testFieldClone = _.cloneDeep(testFields); + const mappings = createMappingsFromPatternFields(testFields); + + expect(mappings.ip).to.not.be(testFields[0]); + expect(_.isEqual(testFields, testFieldClone)).to.be.ok(); + }); + + it('should set the same default mapping for all non-strings', function () { + let mappings = createMappingsFromPatternFields(testFields); + + _.forEach(mappings, function (mapping) { + if (mapping.type !== 'text') { + expect(_.isEqual(mapping, { + type: mapping.type, + index: true, + doc_values: true + })).to.be.ok(); + } + }); + }); + + it('should give strings a multi-field mapping with a "text" base type', function () { + let mappings = createMappingsFromPatternFields(testFields); + + _.forEach(mappings, function (mapping) { + if (mapping.type === 'text') { + expect(mapping).to.have.property('fields'); + } + }); + }); + + it('should handle nested fields', function () { + testFields.push({name: 'geo.coordinates', type: 'geo_point'}); + let mappings = createMappingsFromPatternFields(testFields); + + expect(mappings).to.have.property('geo'); + expect(mappings.geo).to.have.property('properties'); + expect(mappings.geo.properties).to.have.property('coordinates'); + expect(_.isEqual(mappings.geo.properties.coordinates, { + type: 'geo_point', + index: true, + doc_values: true + })).to.be.ok(); + }); + + it('should map all number fields as an ES double', function () { + let mappings = createMappingsFromPatternFields(testFields); + + expect(mappings).to.have.property('bytes'); + expect(mappings.bytes).to.have.property('type', 'double'); + }); +}); diff --git a/src/plugins/kibana/server/lib/__tests__/handle_es_error.js b/src/plugins/kibana/server/lib/__tests__/handle_es_error.js new file mode 100644 index 0000000000000..d45c4a01c796e --- /dev/null +++ b/src/plugins/kibana/server/lib/__tests__/handle_es_error.js @@ -0,0 +1,41 @@ +import expect from 'expect.js'; +import Boom from 'boom'; +import handleESError from '../handle_es_error'; +import { errors as esErrors } from 'elasticsearch'; + +describe('handleESError', function () { + + it('should transform elasticsearch errors into boom errors with the same status code', function () { + const conflict = handleESError(new esErrors.Conflict()); + expect(conflict.isBoom).to.be(true); + expect(conflict.output.statusCode).to.be(409); + + const forbidden = handleESError(new esErrors[403]); + expect(forbidden.isBoom).to.be(true); + expect(forbidden.output.statusCode).to.be(403); + + const notFound = handleESError(new esErrors.NotFound()); + expect(notFound.isBoom).to.be(true); + expect(notFound.output.statusCode).to.be(404); + + const badRequest = handleESError(new esErrors.BadRequest()); + expect(badRequest.isBoom).to.be(true); + expect(badRequest.output.statusCode).to.be(400); + }); + + it('should return an unknown error without transforming it', function () { + const unknown = new Error('mystery error'); + expect(handleESError(unknown)).to.be(unknown); + }); + + it('should return a boom 503 server timeout error for ES connection errors', function () { + expect(handleESError(new esErrors.ConnectionFault()).output.statusCode).to.be(503); + expect(handleESError(new esErrors.ServiceUnavailable()).output.statusCode).to.be(503); + expect(handleESError(new esErrors.NoConnections()).output.statusCode).to.be(503); + expect(handleESError(new esErrors.RequestTimeout()).output.statusCode).to.be(503); + }); + + it('should throw an error if called with a non-error argument', function () { + expect(handleESError).withArgs('notAnError').to.throwException(); + }); +}); diff --git a/src/plugins/kibana/server/lib/__tests__/init_default_field_props.js b/src/plugins/kibana/server/lib/__tests__/init_default_field_props.js new file mode 100644 index 0000000000000..0511b9e2f25d0 --- /dev/null +++ b/src/plugins/kibana/server/lib/__tests__/init_default_field_props.js @@ -0,0 +1,74 @@ +import initDefaultFieldProps from '../init_default_field_props'; +import expect from 'expect.js'; +import _ from 'lodash'; +let fields; + +const testData = [ + { + 'name': 'ip', + 'type': 'ip' + }, { + 'name': '@timestamp', + 'type': 'date' + }, { + 'name': 'agent', + 'type': 'string' + }, { + 'name': 'bytes', + 'type': 'number' + }, + { + 'name': 'geo.coordinates', + 'type': 'geo_point' + } +]; + +describe('initDefaultFieldProps', function () { + + beforeEach(function () { + fields = _.cloneDeep(testData); + }); + + it('should throw an error if no argument is passed or the argument is not an array', function () { + expect(initDefaultFieldProps).to.throwException(/requires an array argument/); + expect(initDefaultFieldProps).withArgs({}).to.throwException(/requires an array argument/); + }); + + it('should set the same defaults for everything but strings', function () { + const results = initDefaultFieldProps(fields); + _.forEach(results, function (field) { + if (field.type !== 'string') { + expect(field).to.have.property('indexed', true); + expect(field).to.have.property('analyzed', false); + expect(field).to.have.property('doc_values', true); + expect(field).to.have.property('scripted', false); + expect(field).to.have.property('count', 0); + } + }); + }); + + it('should make string fields analyzed', function () { + const results = initDefaultFieldProps(fields); + _.forEach(results, function (field) { + if (field.type === 'string' && !_.contains(field.name, 'raw')) { + expect(field).to.have.property('indexed', true); + expect(field).to.have.property('analyzed', true); + expect(field).to.have.property('doc_values', false); + expect(field).to.have.property('scripted', false); + expect(field).to.have.property('count', 0); + } + }); + }); + + it('should create an extra raw non-analyzed field for strings', function () { + const results = initDefaultFieldProps(fields); + const rawField = _.find(results, function (field) { + return _.contains(field.name, 'raw'); + }); + expect(rawField).to.have.property('indexed', true); + expect(rawField).to.have.property('analyzed', false); + expect(rawField).to.have.property('doc_values', true); + expect(rawField).to.have.property('scripted', false); + expect(rawField).to.have.property('count', 0); + }); +}); diff --git a/src/plugins/kibana/server/lib/case_conversion.js b/src/plugins/kibana/server/lib/case_conversion.js new file mode 100644 index 0000000000000..16af43e24581d --- /dev/null +++ b/src/plugins/kibana/server/lib/case_conversion.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +module.exports = { + keysToSnakeCaseShallow: function (object) { + return _.mapKeys(object, (value, key) => { + return _.snakeCase(key); + }); + }, + + keysToCamelCaseShallow: function (object) { + return _.mapKeys(object, (value, key) => { + return _.camelCase(key); + }); + } +}; diff --git a/src/plugins/kibana/server/lib/convert_pattern_and_template_name.js b/src/plugins/kibana/server/lib/convert_pattern_and_template_name.js new file mode 100644 index 0000000000000..2ed68f30ad4ee --- /dev/null +++ b/src/plugins/kibana/server/lib/convert_pattern_and_template_name.js @@ -0,0 +1,22 @@ +// To avoid index template naming collisions the index pattern creation API +// namespaces template names by prepending 'kibana-' to the matching pattern's title. +// e.g. a pattern with title `logstash-*` will have a matching template named `kibana-logstash-*`. +// This module provides utility functions for easily converting between template and pattern names. + +module.exports = { + templateToPattern: (templateName) => { + if (templateName.indexOf('kibana-') === -1) { + throw new Error('not a valid kibana namespaced template name'); + } + + return templateName.slice(templateName.indexOf('-') + 1); + }, + + patternToTemplate: (patternName) => { + if (patternName === '') { + throw new Error('pattern must not be empty'); + } + + return `kibana-${patternName.toLowerCase()}`; + } +}; diff --git a/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js b/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js new file mode 100644 index 0000000000000..b9ee189e35c4a --- /dev/null +++ b/src/plugins/kibana/server/lib/create_mappings_from_pattern_fields.js @@ -0,0 +1,35 @@ +import _ from 'lodash'; + +// Creates an ES field mapping from a single field object in a kibana index pattern +module.exports = function createMappingsFromPatternFields(fields) { + if (_.isEmpty(fields)) { + throw new Error('argument must not be empty'); + } + + const mappings = {}; + + _.forEach(fields, function (field) { + let mapping; + + if (field.type === 'string') { + mapping = { + type: 'text', + fields: { + raw: {type: 'keyword', ignore_above: 256} + } + }; + } + else { + const fieldType = field.type === 'number' ? 'double' : field.type; + mapping = { + type: fieldType, + index: true, + doc_values: true + }; + } + + _.set(mappings, field.name.split('.').join('.properties.'), mapping); + }); + + return mappings; +}; diff --git a/src/plugins/kibana/server/lib/handle_es_error.js b/src/plugins/kibana/server/lib/handle_es_error.js new file mode 100644 index 0000000000000..5e4ba5a967860 --- /dev/null +++ b/src/plugins/kibana/server/lib/handle_es_error.js @@ -0,0 +1,26 @@ +import Boom from 'boom'; +import _ from 'lodash'; +import { errors as esErrors } from 'elasticsearch'; + +module.exports = function handleESError(error) { + if (!(error instanceof Error)) { + throw new Error('Expected an instance of Error'); + } + + if (error instanceof esErrors.ConnectionFault || + error instanceof esErrors.ServiceUnavailable || + error instanceof esErrors.NoConnections || + error instanceof esErrors.RequestTimeout) { + return Boom.serverTimeout(error); + } else if (error instanceof esErrors.Conflict || _.contains(error.message, 'index_template_already_exists')) { + return Boom.conflict(error); + } else if (error instanceof esErrors[403]) { + return Boom.forbidden(error); + } else if (error instanceof esErrors.NotFound) { + return Boom.notFound(error); + } else if (error instanceof esErrors.BadRequest || error instanceof TypeError) { + return Boom.badRequest(error); + } else { + return error; + } +}; diff --git a/src/plugins/kibana/server/lib/init_default_field_props.js b/src/plugins/kibana/server/lib/init_default_field_props.js new file mode 100644 index 0000000000000..aa8109cc01614 --- /dev/null +++ b/src/plugins/kibana/server/lib/init_default_field_props.js @@ -0,0 +1,45 @@ +import _ from 'lodash'; + +module.exports = function initDefaultFieldProps(fields) { + if (fields === undefined || !_.isArray(fields)) { + throw new Error('requires an array argument'); + } + + const results = []; + + _.forEach(fields, function (field) { + const newField = _.cloneDeep(field); + results.push(newField); + + if (newField.type === 'string') { + _.defaults(newField, { + indexed: true, + analyzed: true, + doc_values: false, + scripted: false, + count: 0 + }); + + results.push({ + name: newField.name + '.raw', + type: 'string', + indexed: true, + analyzed: false, + doc_values: true, + scripted: false, + count: 0 + }); + } + else { + _.defaults(newField, { + indexed: true, + analyzed: false, + doc_values: true, + scripted: false, + count: 0 + }); + } + }); + + return results; +}; diff --git a/src/plugins/kibana/server/lib/schemas/resources/index_pattern_schema.js b/src/plugins/kibana/server/lib/schemas/resources/index_pattern_schema.js new file mode 100644 index 0000000000000..76cf5e111726c --- /dev/null +++ b/src/plugins/kibana/server/lib/schemas/resources/index_pattern_schema.js @@ -0,0 +1,23 @@ +import Joi from 'joi'; + +module.exports = Joi.object({ + id: Joi.string().required(), + title: Joi.string().required(), + time_field_name: Joi.string(), + interval_name: Joi.string(), + not_expandable: Joi.boolean(), + fields: Joi.array().items( + Joi.object({ + name: Joi.string().required(), + type: Joi.string().required(), + count: Joi.number().integer(), + scripted: Joi.boolean(), + doc_values: Joi.boolean(), + analyzed: Joi.boolean(), + indexed: Joi.boolean(), + script: Joi.string(), + lang: Joi.string() + }) + ).required().min(1), + field_format_map: Joi.object() +}); diff --git a/src/plugins/kibana/server/routes/api/ingest/index.js b/src/plugins/kibana/server/routes/api/ingest/index.js new file mode 100644 index 0000000000000..860286ccd7194 --- /dev/null +++ b/src/plugins/kibana/server/routes/api/ingest/index.js @@ -0,0 +1,4 @@ +export default function (server) { + require('./register_post')(server); + require('./register_delete')(server); +} diff --git a/src/plugins/kibana/server/routes/api/ingest/register_delete.js b/src/plugins/kibana/server/routes/api/ingest/register_delete.js new file mode 100644 index 0000000000000..fb81655f1da04 --- /dev/null +++ b/src/plugins/kibana/server/routes/api/ingest/register_delete.js @@ -0,0 +1,32 @@ +import Promise from 'bluebird'; +import handleESError from '../../../lib/handle_es_error'; +import {templateToPattern, patternToTemplate} from '../../../lib/convert_pattern_and_template_name'; + +module.exports = function registerDelete(server) { + server.route({ + path: '/api/kibana/ingest/{id}', + method: 'DELETE', + handler: function (req, reply) { + const kibanaIndex = server.config().get('kibana.index'); + const callWithRequest = server.plugins.elasticsearch.callWithRequest; + const deletePatternParams = { + index: kibanaIndex, + type: 'index-pattern', + id: req.params.id + }; + + Promise.all([ + callWithRequest(req, 'delete', deletePatternParams), + callWithRequest(req, 'indices.deleteTemplate', {name: patternToTemplate(req.params.id), ignore: [404]}) + ]) + .then( + function (pattern) { + reply({success: true}); + }, + function (error) { + reply(handleESError(error)); + } + ); + } + }); +}; diff --git a/src/plugins/kibana/server/routes/api/ingest/register_post.js b/src/plugins/kibana/server/routes/api/ingest/register_post.js new file mode 100644 index 0000000000000..a4e259d6efaed --- /dev/null +++ b/src/plugins/kibana/server/routes/api/ingest/register_post.js @@ -0,0 +1,106 @@ +import Boom from 'boom'; +import _ from 'lodash'; +import indexPatternSchema from '../../../lib/schemas/resources/index_pattern_schema'; +import handleESError from '../../../lib/handle_es_error'; +import createMappingsFromPatternFields from '../../../lib/create_mappings_from_pattern_fields'; +import initDefaultFieldProps from '../../../lib/init_default_field_props'; +import {templateToPattern, patternToTemplate} from '../../../lib/convert_pattern_and_template_name'; +import { keysToCamelCaseShallow } from '../../../lib/case_conversion'; + +module.exports = function registerPost(server) { + server.route({ + path: '/api/kibana/ingest', + method: 'POST', + config: { + validate: { + payload: indexPatternSchema + } + }, + handler: function (req, reply) { + const kibanaIndex = server.config().get('kibana.index'); + const callWithRequest = server.plugins.elasticsearch.callWithRequest; + const requestDocument = _.cloneDeep(req.payload); + const indexPatternId = requestDocument.id; + const indexPattern = keysToCamelCaseShallow(requestDocument); + delete indexPattern.id; + + const mappings = createMappingsFromPatternFields(indexPattern.fields); + indexPattern.fields = initDefaultFieldProps(indexPattern.fields); + + indexPattern.fields = JSON.stringify(indexPattern.fields); + indexPattern.fieldFormatMap = JSON.stringify(indexPattern.fieldFormatMap); + + return callWithRequest(req, 'indices.exists', {index: indexPatternId}) + .then((matchingIndices) => { + if (matchingIndices) { + throw Boom.conflict('Cannot create an index pattern via this API if existing indices already match the pattern'); + } + + const patternCreateParams = { + index: kibanaIndex, + type: 'index-pattern', + id: indexPatternId, + body: indexPattern + }; + + return callWithRequest(req, 'create', patternCreateParams) + .then((patternResponse) => { + const templateParams = { + order: 0, + create: true, + name: patternToTemplate(indexPatternId), + body: { + template: indexPatternId, + mappings: { + _default_: { + dynamic_templates: [{ + string_fields: { + match: '*', + match_mapping_type: 'string', + mapping: { + type: 'text', + fields: { + raw: {type: 'keyword', ignore_above: 256} + } + } + } + }], + properties: mappings + } + } + } + }; + + return callWithRequest(req, 'indices.putTemplate', templateParams) + .catch((templateError) => { + const deleteParams = { + index: kibanaIndex, + type: 'index-pattern', + id: indexPatternId + }; + + return callWithRequest(req, 'delete', deleteParams) + .then(() => { + throw templateError; + }, (patternDeletionError) => { + throw new Error( + `index-pattern ${indexPatternId} created successfully but index template + creation failed. Failed to rollback index-pattern creation, must delete manually. + ${patternDeletionError.toString()} + ${templateError.toString()}` + ); + }); + }); + }); + }) + .then( + function () { + reply().code(204); + }, + function (error) { + reply(handleESError(error)); + } + ); + } + }); +}; diff --git a/src/plugins/kibana/server/routes/api/search/count/register_count.js b/src/plugins/kibana/server/routes/api/search/count/register_count.js new file mode 100644 index 0000000000000..8e542afe70cf0 --- /dev/null +++ b/src/plugins/kibana/server/routes/api/search/count/register_count.js @@ -0,0 +1,25 @@ +import _ from 'lodash'; +import handleESError from '../../../../lib/handle_es_error'; + +export default function registerCount(server) { + server.route({ + path: '/api/kibana/{id}/_count', + method: ['POST', 'GET'], + handler: function (req, reply) { + const boundCallWithRequest = _.partial(server.plugins.elasticsearch.callWithRequest, req); + + boundCallWithRequest('count', { + allowNoIndices: false, + index: req.params.id + }) + .then( + function (res) { + reply({count: res.count}); + }, + function (error) { + reply(handleESError(error)); + } + ); + } + }); +} diff --git a/src/plugins/kibana/server/routes/api/search/index.js b/src/plugins/kibana/server/routes/api/search/index.js new file mode 100644 index 0000000000000..2dfea0d380d4f --- /dev/null +++ b/src/plugins/kibana/server/routes/api/search/index.js @@ -0,0 +1,5 @@ +import registerCount from './count/register_count'; + +export default function (server) { + registerCount(server); +} diff --git a/src/plugins/kibana/server/routes/api/settings/index.js b/src/plugins/kibana/server/routes/api/settings/index.js new file mode 100644 index 0000000000000..d6c0df42ff44f --- /dev/null +++ b/src/plugins/kibana/server/routes/api/settings/index.js @@ -0,0 +1,6 @@ +export default function (server) { + require('./register_get')(server); + require('./register_set')(server); + require('./register_set_many')(server); + require('./register_delete')(server); +} diff --git a/src/plugins/kibana/server/routes/api/settings/register_delete.js b/src/plugins/kibana/server/routes/api/settings/register_delete.js new file mode 100644 index 0000000000000..46c675c9e9650 --- /dev/null +++ b/src/plugins/kibana/server/routes/api/settings/register_delete.js @@ -0,0 +1,19 @@ +import Boom from 'boom'; + +export default function registerDelete(server) { + server.route({ + path: '/api/kibana/settings/{key}', + method: 'DELETE', + handler: function (req, reply) { + const { key } = req.params; + const uiSettings = server.uiSettings(); + uiSettings + .remove(key) + .then(() => uiSettings + .getUserProvided() + .then(settings => reply({ settings }).type('application/json')) + ) + .catch(reason => reply(Boom.wrap(reason))); + } + }); +} diff --git a/src/plugins/kibana/server/routes/api/settings/register_get.js b/src/plugins/kibana/server/routes/api/settings/register_get.js new file mode 100644 index 0000000000000..217dc3936430e --- /dev/null +++ b/src/plugins/kibana/server/routes/api/settings/register_get.js @@ -0,0 +1,15 @@ +import Boom from 'boom'; + +export default function registerGet(server) { + server.route({ + path: '/api/kibana/settings', + method: 'GET', + handler: function (req, reply) { + server + .uiSettings() + .getUserProvided() + .then(settings => reply({ settings }).type('application/json')) + .catch(reason => reply(Boom.wrap(reason))); + } + }); +} diff --git a/src/plugins/kibana/server/routes/api/settings/register_set.js b/src/plugins/kibana/server/routes/api/settings/register_set.js new file mode 100644 index 0000000000000..51dc364c6c2b9 --- /dev/null +++ b/src/plugins/kibana/server/routes/api/settings/register_set.js @@ -0,0 +1,20 @@ +import Boom from 'boom'; + +export default function registerSet(server) { + server.route({ + path: '/api/kibana/settings/{key}', + method: 'POST', + handler: function (req, reply) { + const { key } = req.params; + const { value } = req.payload; + const uiSettings = server.uiSettings(); + uiSettings + .set(key, value) + .then(() => uiSettings + .getUserProvided() + .then(settings => reply({ settings }).type('application/json')) + ) + .catch(reason => reply(Boom.wrap(reason))); + } + }); +} diff --git a/src/plugins/kibana/server/routes/api/settings/register_set_many.js b/src/plugins/kibana/server/routes/api/settings/register_set_many.js new file mode 100644 index 0000000000000..7ab2c035de76f --- /dev/null +++ b/src/plugins/kibana/server/routes/api/settings/register_set_many.js @@ -0,0 +1,20 @@ +import Boom from 'boom'; + +export default function registerSet(server) { + server.route({ + path: '/api/kibana/settings', + method: 'POST', + handler: function (req, reply) { + const { key } = req.params; + const { changes } = req.payload; + const uiSettings = server.uiSettings(); + uiSettings + .setMany(changes) + .then(() => uiSettings + .getUserProvided() + .then(settings => reply({ settings }).type('application/json')) + ) + .catch(reason => reply(Boom.wrap(reason))); + } + }); +} diff --git a/src/plugins/markdown_vis/index.js b/src/plugins/markdown_vis/index.js index 85c44ec5b2b40..973760d27889c 100644 --- a/src/plugins/markdown_vis/index.js +++ b/src/plugins/markdown_vis/index.js @@ -1,4 +1,4 @@ -module.exports = function (kibana) { +export default function (kibana) { return new kibana.Plugin({ diff --git a/src/plugins/markdown_vis/public/__tests__/markdown_vis_controller.js b/src/plugins/markdown_vis/public/__tests__/markdown_vis_controller.js index 97f9e5a019d79..93fafef12882c 100644 --- a/src/plugins/markdown_vis/public/__tests__/markdown_vis_controller.js +++ b/src/plugins/markdown_vis/public/__tests__/markdown_vis_controller.js @@ -1,9 +1,9 @@ +import ngMock from 'ng_mock'; +import expect from 'expect.js'; describe('markdown vis controller', function () { - var $scope; - var $el; - var ngMock = require('ngMock'); - var expect = require('expect.js'); + let $scope; + let $el; beforeEach(ngMock.module('kibana/markdown_vis')); beforeEach(ngMock.inject(function ($rootScope, $controller) { diff --git a/src/plugins/markdown_vis/public/markdown_vis.js b/src/plugins/markdown_vis/public/markdown_vis.js index 3066cd8c108af..4dd89fad52367 100644 --- a/src/plugins/markdown_vis/public/markdown_vis.js +++ b/src/plugins/markdown_vis/public/markdown_vis.js @@ -1,31 +1,32 @@ -define(function (require) { - // we need to load the css ourselves - require('plugins/markdown_vis/markdown_vis.less'); +import 'plugins/markdown_vis/markdown_vis.less'; +import 'plugins/markdown_vis/markdown_vis_controller'; +import TemplateVisTypeTemplateVisTypeProvider from 'ui/template_vis_type/template_vis_type'; +import markdownVisTemplate from 'plugins/markdown_vis/markdown_vis.html'; +import markdownVisParamsTemplate from 'plugins/markdown_vis/markdown_vis_params.html'; +// we need to load the css ourselves - // we also need to load the controller and used by the template - require('plugins/markdown_vis/markdown_vis_controller'); +// we also need to load the controller and used by the template - // register the provider with the visTypes registry so that other know it exists - require('ui/registry/vis_types').register(MarkdownVisProvider); +// register the provider with the visTypes registry so that other know it exists +require('ui/registry/vis_types').register(MarkdownVisProvider); - function MarkdownVisProvider(Private) { - var TemplateVisType = Private(require('ui/template_vis_type/TemplateVisType')); +function MarkdownVisProvider(Private) { + const TemplateVisType = Private(TemplateVisTypeTemplateVisTypeProvider); - // return the visType object, which kibana will use to display and configure new - // Vis object of this type. - return new TemplateVisType({ - name: 'markdown', - title: 'Markdown widget', - icon: 'fa-code', - description: 'Useful for displaying explanations or instructions for dashboards.', - template: require('plugins/markdown_vis/markdown_vis.html'), - params: { - editor: require('plugins/markdown_vis/markdown_vis_params.html') - }, - requiresSearch: false - }); - } + // return the visType object, which kibana will use to display and configure new + // Vis object of this type. + return new TemplateVisType({ + name: 'markdown', + title: 'Markdown widget', + icon: 'fa-code', + description: 'Useful for displaying explanations or instructions for dashboards.', + template: markdownVisTemplate, + params: { + editor: markdownVisParamsTemplate + }, + requiresSearch: false + }); +} - // export the provider so that the visType can be required with Private() - return MarkdownVisProvider; -}); +// export the provider so that the visType can be required with Private() +export default MarkdownVisProvider; diff --git a/src/plugins/markdown_vis/public/markdown_vis_controller.js b/src/plugins/markdown_vis/public/markdown_vis_controller.js index d46d278520beb..de9e0a5902e9f 100644 --- a/src/plugins/markdown_vis/public/markdown_vis_controller.js +++ b/src/plugins/markdown_vis/public/markdown_vis_controller.js @@ -1,15 +1,14 @@ -define(function (require) { - var marked = require('marked'); - marked.setOptions({ - gfm: true, // Github-flavored markdown - sanitize: true // Sanitize HTML tags - }); +import marked from 'marked'; +import uiModules from 'ui/modules'; +marked.setOptions({ + gfm: true, // Github-flavored markdown + sanitize: true // Sanitize HTML tags +}); - var module = require('ui/modules').get('kibana/markdown_vis', ['kibana']); - module.controller('KbnMarkdownVisController', function ($scope, $sce) { - $scope.$watch('vis.params.markdown', function (html) { - if (!html) return; - $scope.html = $sce.trustAsHtml(marked(html)); - }); +const module = uiModules.get('kibana/markdown_vis', ['kibana']); +module.controller('KbnMarkdownVisController', function ($scope, $sce) { + $scope.$watch('vis.params.markdown', function (html) { + if (!html) return; + $scope.html = $sce.trustAsHtml(marked(html)); }); }); diff --git a/src/plugins/metric_vis/index.js b/src/plugins/metric_vis/index.js index 129211697e149..4824fe245d402 100644 --- a/src/plugins/metric_vis/index.js +++ b/src/plugins/metric_vis/index.js @@ -1,4 +1,4 @@ -module.exports = function (kibana) { +export default function (kibana) { return new kibana.Plugin({ diff --git a/src/plugins/metric_vis/public/__tests__/metric_vis_controller.js b/src/plugins/metric_vis/public/__tests__/metric_vis_controller.js index b1309ff569ebc..a745d90c7f841 100644 --- a/src/plugins/metric_vis/public/__tests__/metric_vis_controller.js +++ b/src/plugins/metric_vis/public/__tests__/metric_vis_controller.js @@ -1,10 +1,10 @@ +import ngMock from 'ng_mock'; +import expect from 'expect.js'; describe('metric vis', function () { - var $scope; - var ngMock = require('ngMock'); - var expect = require('expect.js'); + let $scope; - var formatter = function (value) { + const formatter = function (value) { return value.toFixed(3); }; diff --git a/src/plugins/metric_vis/public/metric_vis.html b/src/plugins/metric_vis/public/metric_vis.html index ef30d0404d8c1..c1d9150b679a2 100644 --- a/src/plugins/metric_vis/public/metric_vis.html +++ b/src/plugins/metric_vis/public/metric_vis.html @@ -1,6 +1,6 @@
    -
    -
    {{metric.value}}
    -
    {{metric.label}}
    -
    +
    +
    {{metric.value}}
    +
    {{metric.label}}
    +
    diff --git a/src/plugins/metric_vis/public/metric_vis.js b/src/plugins/metric_vis/public/metric_vis.js index 0e5b99c4e225f..f73b9ffe5a7a9 100644 --- a/src/plugins/metric_vis/public/metric_vis.js +++ b/src/plugins/metric_vis/public/metric_vis.js @@ -1,46 +1,49 @@ -define(function (require) { - // we need to load the css ourselves - require('plugins/metric_vis/metric_vis.less'); +import 'plugins/metric_vis/metric_vis.less'; +import 'plugins/metric_vis/metric_vis_controller'; +import TemplateVisTypeTemplateVisTypeProvider from 'ui/template_vis_type/template_vis_type'; +import VisSchemasProvider from 'ui/vis/schemas'; +import metricVisTemplate from 'plugins/metric_vis/metric_vis.html'; +import metricVisParamsTemplate from 'plugins/metric_vis/metric_vis_params.html'; +// we need to load the css ourselves - // we also need to load the controller and used by the template - require('plugins/metric_vis/metric_vis_controller'); +// we also need to load the controller and used by the template - // register the provider with the visTypes registry - require('ui/registry/vis_types').register(MetricVisProvider); +// register the provider with the visTypes registry +require('ui/registry/vis_types').register(MetricVisProvider); - function MetricVisProvider(Private) { - var TemplateVisType = Private(require('ui/template_vis_type/TemplateVisType')); - var Schemas = Private(require('ui/Vis/Schemas')); +function MetricVisProvider(Private) { + const TemplateVisType = Private(TemplateVisTypeTemplateVisTypeProvider); + const Schemas = Private(VisSchemasProvider); - // return the visType object, which kibana will use to display and configure new - // Vis object of this type. - return new TemplateVisType({ - name: 'metric', - title: 'Metric', - description: 'One big number for all of your one big number needs. Perfect for showing ' + - 'a count of hits, or the exact average a numeric field.', - icon: 'fa-calculator', - template: require('plugins/metric_vis/metric_vis.html'), - params: { - defaults: { - fontSize: 60 - }, - editor: require('plugins/metric_vis/metric_vis_params.html') + // return the visType object, which kibana will use to display and configure new + // Vis object of this type. + return new TemplateVisType({ + name: 'metric', + title: 'Metric', + description: 'One big number for all of your one big number needs. Perfect for showing ' + + 'a count of hits, or the exact average a numeric field.', + icon: 'fa-calculator', + template: metricVisTemplate, + params: { + defaults: { + handleNoResults: true, + fontSize: 60 }, - schemas: new Schemas([ - { - group: 'metrics', - name: 'metric', - title: 'Metric', - min: 1, - defaults: [ - { type: 'count', schema: 'metric' } - ] - } - ]) - }); - } + editor: metricVisParamsTemplate + }, + schemas: new Schemas([ + { + group: 'metrics', + name: 'metric', + title: 'Metric', + min: 1, + defaults: [ + { type: 'count', schema: 'metric' } + ] + } + ]) + }); +} - // export the provider so that the visType can be required with Private() - return MetricVisProvider; -}); +// export the provider so that the visType can be required with Private() +export default MetricVisProvider; diff --git a/src/plugins/metric_vis/public/metric_vis.less b/src/plugins/metric_vis/public/metric_vis.less index bc0b7244a5e34..88abea610948d 100644 --- a/src/plugins/metric_vis/public/metric_vis.less +++ b/src/plugins/metric_vis/public/metric_vis.less @@ -16,6 +16,5 @@ .metric-container { text-align: center; - padding: 1em; } } diff --git a/src/plugins/metric_vis/public/metric_vis_controller.js b/src/plugins/metric_vis/public/metric_vis_controller.js index 3ac2ef17c7d22..41f58f7f6380c 100644 --- a/src/plugins/metric_vis/public/metric_vis_controller.js +++ b/src/plugins/metric_vis/public/metric_vis_controller.js @@ -1,30 +1,39 @@ -define(function (require) { - // get the kibana/metric_vis module, and make sure that it requires the "kibana" module if it - // didn't already - var module = require('ui/modules').get('kibana/metric_vis', ['kibana']); +import _ from 'lodash'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import uiModules from 'ui/modules'; +// get the kibana/metric_vis module, and make sure that it requires the "kibana" module if it +// didn't already +const module = uiModules.get('kibana/metric_vis', ['kibana']); - module.controller('KbnMetricVisController', function ($scope, Private) { - var tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); +module.controller('KbnMetricVisController', function ($scope, Private) { + const tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); - var metrics = $scope.metrics = []; + const metrics = $scope.metrics = []; - $scope.processTableGroups = function (tableGroups) { - tableGroups.tables.forEach(function (table) { - table.columns.forEach(function (column, i) { - var fieldFormatter = table.aggConfig(column).fieldFormatter(); - metrics.push({ - label: column.title, - value: fieldFormatter(table.rows[0][i]) - }); + function isInvalid(val) { + return _.isUndefined(val) || _.isNull(val) || _.isNaN(val); + } + + $scope.processTableGroups = function (tableGroups) { + tableGroups.tables.forEach(function (table) { + table.columns.forEach(function (column, i) { + const fieldFormatter = table.aggConfig(column).fieldFormatter(); + let value = table.rows[0][i]; + + value = isInvalid(value) ? '?' : fieldFormatter(value); + + metrics.push({ + label: column.title, + value: value }); }); - }; - - $scope.$watch('esResponse', function (resp) { - if (resp) { - metrics.length = 0; - $scope.processTableGroups(tabifyAggResponse($scope.vis, resp)); - } }); + }; + + $scope.$watch('esResponse', function (resp) { + if (resp) { + metrics.length = 0; + $scope.processTableGroups(tabifyAggResponse($scope.vis, resp)); + } }); }); diff --git a/src/plugins/spyModes/index.js b/src/plugins/spyModes/index.js deleted file mode 100644 index cbcc86e2012da..0000000000000 --- a/src/plugins/spyModes/index.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = function (kibana) { - return new kibana.Plugin({ - uiExports: { - spyModes: [ - 'plugins/spyModes/tableSpyMode', - 'plugins/spyModes/reqRespStatsSpyMode' - ] - } - }); -}; diff --git a/src/plugins/spyModes/package.json b/src/plugins/spyModes/package.json deleted file mode 100644 index e5f8c52a1f4a8..0000000000000 --- a/src/plugins/spyModes/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "spyModes", - "version": "1.0.0" -} diff --git a/src/plugins/spyModes/public/reqRespStatsSpyMode.js b/src/plugins/spyModes/public/reqRespStatsSpyMode.js deleted file mode 100644 index 471026beca368..0000000000000 --- a/src/plugins/spyModes/public/reqRespStatsSpyMode.js +++ /dev/null @@ -1,59 +0,0 @@ -define(function (require) { - var _ = require('lodash'); - var reqRespStatsHTML = require('plugins/spyModes/reqRespStatsSpyMode.html'); - - var linkReqRespStats = function ($scope, config) { - $scope.$bind('req', 'searchSource.history[searchSource.history.length - 1]'); - $scope.$watchMulti([ - 'req', - 'req.started', - 'req.stopped', - 'searchSource' - ], function () { - if (!$scope.searchSource || !$scope.req) return; - - var req = $scope.req; - var resp = $scope.req.resp; - var stats = $scope.stats = []; - - if (resp && resp.took != null) stats.push(['Query Duration', resp.took + 'ms']); - if (req && req.ms != null) stats.push(['Request Duration', req.ms + 'ms']); - if (resp && resp.hits) stats.push(['Hits', resp.hits.total]); - - if (req.fetchParams) { - if (req.fetchParams.index) stats.push(['Index', req.fetchParams.index]); - if (req.fetchParams.type) stats.push(['Type', req.fetchParams.type]); - if (req.fetchParams.id) stats.push(['Id', req.fetchParams.id]); - } - }); - }; - - require('ui/registry/spy_modes') - .register(function () { - return { - name: 'request', - display: 'Request', - order: 2, - template: reqRespStatsHTML, - link: linkReqRespStats - }; - }) - .register(function () { - return { - name: 'response', - display: 'Response', - order: 3, - template: reqRespStatsHTML, - link: linkReqRespStats - }; - }) - .register(function () { - return { - name: 'stats', - display: 'Statistics', - order: 4, - template: reqRespStatsHTML, - link: linkReqRespStats - }; - }); -}); diff --git a/src/plugins/spyModes/public/tableSpyMode.html b/src/plugins/spyModes/public/tableSpyMode.html deleted file mode 100644 index 2a70eacea7498..0000000000000 --- a/src/plugins/spyModes/public/tableSpyMode.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/plugins/spyModes/public/tableSpyMode.js b/src/plugins/spyModes/public/tableSpyMode.js deleted file mode 100644 index 97eac984b0914..0000000000000 --- a/src/plugins/spyModes/public/tableSpyMode.js +++ /dev/null @@ -1,40 +0,0 @@ -define(function (require) { - function VisSpyTableProvider(Notifier, $filter, $rootScope, config, Private) { - var _ = require('lodash'); - var saveAs = require('@spalger/filesaver').saveAs; - var tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); - - var PER_PAGE_DEFAULT = 10; - - require('ui/agg_table'); - - return { - name: 'table', - display: 'Table', - order: 1, - template: require('plugins/spyModes/tableSpyMode.html'), - link: function tableLinkFn($scope, $el) { - $rootScope.$watchMulti.call($scope, [ - 'vis', - 'esResp' - ], function () { - if (!$scope.vis || !$scope.esResp) { - $scope.table = null; - } else { - if (!$scope.editableVis.params.spyPerPage) { - $scope.editableVis.params.spyPerPage = PER_PAGE_DEFAULT; - } - - $scope.table = tabifyAggResponse($scope.vis, $scope.esResp, { - canSplit: false, - asAggConfigResults: true, - partialRows: true - }); - } - }); - } - }; - } - - require('ui/registry/spy_modes').register(VisSpyTableProvider); -}); diff --git a/src/plugins/spy_modes/index.js b/src/plugins/spy_modes/index.js new file mode 100644 index 0000000000000..1de9c844b168d --- /dev/null +++ b/src/plugins/spy_modes/index.js @@ -0,0 +1,10 @@ +export default function (kibana) { + return new kibana.Plugin({ + uiExports: { + spyModes: [ + 'plugins/spy_modes/table_spy_mode', + 'plugins/spy_modes/req_resp_stats_spy_mode' + ] + } + }); +}; diff --git a/src/plugins/spy_modes/package.json b/src/plugins/spy_modes/package.json new file mode 100644 index 0000000000000..8bf42bfa865a7 --- /dev/null +++ b/src/plugins/spy_modes/package.json @@ -0,0 +1,4 @@ +{ + "name": "spy_modes", + "version": "1.0.0" +} diff --git a/src/plugins/spyModes/public/reqRespStatsSpyMode.html b/src/plugins/spy_modes/public/req_resp_stats_spy_mode.html similarity index 100% rename from src/plugins/spyModes/public/reqRespStatsSpyMode.html rename to src/plugins/spy_modes/public/req_resp_stats_spy_mode.html diff --git a/src/plugins/spy_modes/public/req_resp_stats_spy_mode.js b/src/plugins/spy_modes/public/req_resp_stats_spy_mode.js new file mode 100644 index 0000000000000..2084d42a430db --- /dev/null +++ b/src/plugins/spy_modes/public/req_resp_stats_spy_mode.js @@ -0,0 +1,57 @@ +import _ from 'lodash'; +import reqRespStatsHTML from 'plugins/spy_modes/req_resp_stats_spy_mode.html'; + +const linkReqRespStats = function ($scope, config) { + $scope.$bind('req', 'searchSource.history[searchSource.history.length - 1]'); + $scope.$watchMulti([ + 'req', + 'req.started', + 'req.stopped', + 'searchSource' + ], function () { + if (!$scope.searchSource || !$scope.req) return; + + const req = $scope.req; + const resp = $scope.req.resp; + const stats = $scope.stats = []; + + if (resp && resp.took != null) stats.push(['Query Duration', resp.took + 'ms']); + if (req && req.ms != null) stats.push(['Request Duration', req.ms + 'ms']); + if (resp && resp.hits) stats.push(['Hits', resp.hits.total]); + + if (req.fetchParams) { + if (req.fetchParams.index) stats.push(['Index', req.fetchParams.index]); + if (req.fetchParams.type) stats.push(['Type', req.fetchParams.type]); + if (req.fetchParams.id) stats.push(['Id', req.fetchParams.id]); + } + }); +}; + +require('ui/registry/spy_modes') +.register(function () { + return { + name: 'request', + display: 'Request', + order: 2, + template: reqRespStatsHTML, + link: linkReqRespStats + }; +}) +.register(function () { + return { + name: 'response', + display: 'Response', + order: 3, + template: reqRespStatsHTML, + link: linkReqRespStats + }; +}) +.register(function () { + return { + name: 'stats', + display: 'Statistics', + order: 4, + template: reqRespStatsHTML, + link: linkReqRespStats + }; +}); diff --git a/src/plugins/spy_modes/public/table_spy_mode.html b/src/plugins/spy_modes/public/table_spy_mode.html new file mode 100644 index 0000000000000..e541921c24765 --- /dev/null +++ b/src/plugins/spy_modes/public/table_spy_mode.html @@ -0,0 +1,5 @@ + + diff --git a/src/plugins/spy_modes/public/table_spy_mode.js b/src/plugins/spy_modes/public/table_spy_mode.js new file mode 100644 index 0000000000000..f786614074779 --- /dev/null +++ b/src/plugins/spy_modes/public/table_spy_mode.js @@ -0,0 +1,40 @@ +import { saveAs } from '@spalger/filesaver'; +import _ from 'lodash'; +import 'ui/agg_table'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import tableSpyModeTemplate from 'plugins/spy_modes/table_spy_mode.html'; +function VisSpyTableProvider(Notifier, $filter, $rootScope, config, Private) { + const tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); + + const PER_PAGE_DEFAULT = 10; + + + return { + name: 'table', + display: 'Table', + order: 1, + template: tableSpyModeTemplate, + link: function tableLinkFn($scope, $el) { + $rootScope.$watchMulti.call($scope, [ + 'vis', + 'esResp' + ], function () { + if (!$scope.vis || !$scope.esResp) { + $scope.table = null; + } else { + if (!$scope.spy.params.spyPerPage) { + $scope.spy.params.spyPerPage = PER_PAGE_DEFAULT; + } + + $scope.table = tabifyAggResponse($scope.vis, $scope.esResp, { + canSplit: false, + asAggConfigResults: true, + partialRows: true + }); + } + }); + } + }; +} + +require('ui/registry/spy_modes').register(VisSpyTableProvider); diff --git a/src/plugins/statusPage/index.js b/src/plugins/statusPage/index.js deleted file mode 100644 index 49f139afd46b0..0000000000000 --- a/src/plugins/statusPage/index.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = function (kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Server Status', - main: 'plugins/statusPage/statusPage', - hidden: true, - url: '/status', - - autoload: [].concat( - kibana.autoload.styles, - 'ui/chrome', - 'angular' - ) - } - } - }); -}; diff --git a/src/plugins/statusPage/package.json b/src/plugins/statusPage/package.json deleted file mode 100644 index 853b935ec57e9..0000000000000 --- a/src/plugins/statusPage/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "statusPage", - "version": "1.0.0" -} diff --git a/src/plugins/statusPage/public/lib/formatNumber.js b/src/plugins/statusPage/public/lib/formatNumber.js deleted file mode 100644 index 291ea77ae7d4b..0000000000000 --- a/src/plugins/statusPage/public/lib/formatNumber.js +++ /dev/null @@ -1,19 +0,0 @@ - -var moment = require('moment'); -var numeral = require('numeral'); - -module.exports = function formatNumber(num, which) { - var format = '0.00'; - var postfix = ''; - switch (which) { - case 'time': - return moment(num).format('HH:mm:ss'); - case 'byte': - format += ' b'; - break; - case 'ms': - postfix = ' ms'; - break; - } - return numeral(num).format(format) + postfix; -}; diff --git a/src/plugins/statusPage/public/lib/makeChartOptions.js b/src/plugins/statusPage/public/lib/makeChartOptions.js deleted file mode 100644 index b7a8783c84396..0000000000000 --- a/src/plugins/statusPage/public/lib/makeChartOptions.js +++ /dev/null @@ -1,28 +0,0 @@ - -var formatNumber = require('./formatNumber'); - -module.exports = function makeChartOptions(type) { - return { - chart: { - type: 'lineChart', - height: 200, - showLegend: false, - showXAxis: false, - showYAxis: false, - useInteractiveGuideline: true, - tooltips: true, - pointSize: 0, - color: ['#444', '#777', '#aaa'], - margin: { - top: 10, - left: 0, - right: 0, - bottom: 20 - }, - xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } }, - yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, }, - y: function (d) { return d.y; }, - x: function (d) { return d.x; } - } - }; -}; diff --git a/src/plugins/statusPage/public/lib/readStatData.js b/src/plugins/statusPage/public/lib/readStatData.js deleted file mode 100644 index 1bfe56392fe29..0000000000000 --- a/src/plugins/statusPage/public/lib/readStatData.js +++ /dev/null @@ -1,39 +0,0 @@ -var _ = require('lodash'); - -module.exports = function readStatData(data, seriesNames) { - // Metric Values format - // metric: [[xValue, yValue], ...] - // LoadMetric: - // metric: [[xValue, [yValue, yValue2, yValue3]], ...] - // return [ - // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]}, - // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]}, - // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}] - // - // Go through all of the metric values and split the values out. - // returns an array of all of the averages - - var metricList = []; - seriesNames = seriesNames || []; - data.forEach(function (vector) { - vector = _.flatten(vector); - var x = vector.shift(); - vector.forEach(function (yValue, i) { - var series = seriesNames[i] || ''; - - if (!metricList[i]) { - metricList[i] = { - key: series, - values: [] - }; - } - // unshift to make sure they're in the correct order - metricList[i].values.unshift({ - x: x, - y: yValue - }); - }); - }); - - return metricList; -}; diff --git a/src/plugins/statusPage/public/lib/toTitleCase.js b/src/plugins/statusPage/public/lib/toTitleCase.js deleted file mode 100644 index e7f4a1d33a4fa..0000000000000 --- a/src/plugins/statusPage/public/lib/toTitleCase.js +++ /dev/null @@ -1,10 +0,0 @@ -var _ = require('lodash'); - -// Turns thisIsASentence to -// This Is A Sentence -module.exports = function toTitleCase(name) { - return name - .split(/(?=[A-Z])/) - .map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); }) - .join(' '); -}; diff --git a/src/plugins/statusPage/public/statusPage.html b/src/plugins/statusPage/public/statusPage.html deleted file mode 100644 index 33ebdcc6fff7c..0000000000000 --- a/src/plugins/statusPage/public/statusPage.html +++ /dev/null @@ -1,39 +0,0 @@ -
    -
    -

    - Status: {{ ui.serverStateMessage }} - -

    -
    - -
    -
    - -
    -
    - -
    -

    Installed Plugins

    -
    - -
    - -

    - No plugin status information available -

    - - - - - - - - - - -
    NameStatus
    {{status.name}} - - {{status.message}} -
    -
    -
    diff --git a/src/plugins/statusPage/public/statusPage.js b/src/plugins/statusPage/public/statusPage.js deleted file mode 100644 index 6189ec0832581..0000000000000 --- a/src/plugins/statusPage/public/statusPage.js +++ /dev/null @@ -1,55 +0,0 @@ -var $ = require('jquery'); -var _ = require('lodash'); -var notify = require('ui/notify'); - -require('plugins/statusPage/statusPageMetric'); -require('plugins/statusPage/statusPage.less'); - -var chrome = require('ui/chrome') -.setTabs([ - { - id: '', - title: 'Server Status', - activeIndicatorColor: '#EFF0F2' - } -]) -.setRootTemplate(require('plugins/statusPage/statusPage.html')) -.setRootController('ui', function ($http, $scope) { - var ui = this; - ui.loading = false; - - ui.refresh = function () { - ui.loading = true; - - // go ahead and get the info you want - return $http - .get(chrome.addBasePath('/api/status')) - .then(function (resp) { - - if (ui.fetchError) { - ui.fetchError.clear(); - ui.fetchError = null; - } - - var data = resp.data; - ui.metrics = data.metrics; - ui.statuses = data.status.statuses; - - var overall = data.status.overall; - if (!ui.serverState || (ui.serverState !== overall.state)) { - ui.serverState = overall.state; - ui.serverStateMessage = overall.title; - } - }) - .catch(function () { - if (ui.fetchError) return; - ui.fetchError = notify.error('Failed to request server ui. Perhaps your server is down?'); - ui.metrics = ui.statuses = ui.overall = null; - }) - .then(function () { - ui.loading = false; - }); - }; - - ui.refresh(); -}); diff --git a/src/plugins/statusPage/public/statusPageMetric.js b/src/plugins/statusPage/public/statusPageMetric.js deleted file mode 100644 index 99de1c3670240..0000000000000 --- a/src/plugins/statusPage/public/statusPageMetric.js +++ /dev/null @@ -1,73 +0,0 @@ -var _ = require('lodash'); -var moment = require('moment'); -var numeral = require('numeral'); - -var toTitleCase = require('./lib/toTitleCase'); -var formatNumber = require('./lib/formatNumber'); -var readStatData = require('./lib/readStatData'); - -function calcAvg(metricList, metricNumberType) { - return metricList.map(function (data) { - var uglySum = data.values.reduce(function (sumSoFar, vector) { - return sumSoFar + vector.y; - }, 0); - return formatNumber(uglySum / data.values.length, metricNumberType); - }); -} - -require('ui/modules') -.get('kibana', []) -.directive('statusPageMetric', function () { - return { - restrict: 'E', - template: require('plugins/statusPage/statusPageMetric.html'), - scope: { - name: '@', - data: '=' - }, - controllerAs: 'metric', - controller: function ($scope) { - var self = this; - - self.name = $scope.name; - self.title = toTitleCase(self.name); - self.extendedTitle = self.title; - self.numberType = 'precise'; - self.seriesNames = []; - - switch (self.name) { - case 'heapTotal': - case 'heapUsed': - self.numberType = 'byte'; - break; - - case 'responseTimeAvg': - case 'responseTimeMax': - self.numberType = 'ms'; - break; - - case 'load': - self.seriesNames = ['1min', '5min', '15min']; - break; - } - - $scope.$watch('data', function (data) { - self.rawData = data; - self.chartData = readStatData(self.rawData, self.seriesNames); - self.averages = calcAvg(self.chartData, self.numberType); - - var unit = ''; - self.averages = self.averages.map(function (average) { - var parts = average.split(' '); - var value = parts.shift(); - unit = parts.join(' '); - return value; - }); - self.extendedTitle = self.title; - if (unit) { - self.extendedTitle = `${self.extendedTitle} (${unit})`; - } - }); - } - }; -}); diff --git a/src/plugins/status_page/index.js b/src/plugins/status_page/index.js new file mode 100644 index 0000000000000..cf9ebb95f2f09 --- /dev/null +++ b/src/plugins/status_page/index.js @@ -0,0 +1,12 @@ +export default function (kibana) { + return new kibana.Plugin({ + uiExports: { + app: { + title: 'Server Status', + main: 'plugins/status_page/status_page', + hidden: true, + url: '/status' + } + } + }); +}; diff --git a/src/plugins/status_page/package.json b/src/plugins/status_page/package.json new file mode 100644 index 0000000000000..551751fda01ca --- /dev/null +++ b/src/plugins/status_page/package.json @@ -0,0 +1,4 @@ +{ + "name": "status_page", + "version": "1.0.0" +} diff --git a/src/plugins/status_page/public/lib/format_number.js b/src/plugins/status_page/public/lib/format_number.js new file mode 100644 index 0000000000000..a5e0a7f72bfd3 --- /dev/null +++ b/src/plugins/status_page/public/lib/format_number.js @@ -0,0 +1,19 @@ + +import moment from 'moment'; +import numeral from 'numeral'; + +module.exports = function formatNumber(num, which) { + let format = '0.00'; + let postfix = ''; + switch (which) { + case 'time': + return moment(num).format('HH:mm:ss'); + case 'byte': + format += ' b'; + break; + case 'ms': + postfix = ' ms'; + break; + } + return numeral(num).format(format) + postfix; +}; diff --git a/src/plugins/status_page/public/lib/make_chart_options.js b/src/plugins/status_page/public/lib/make_chart_options.js new file mode 100644 index 0000000000000..e5cf46f2602dd --- /dev/null +++ b/src/plugins/status_page/public/lib/make_chart_options.js @@ -0,0 +1,28 @@ + +import formatNumber from './format_number'; + +module.exports = function makeChartOptions(type) { + return { + chart: { + type: 'lineChart', + height: 200, + showLegend: false, + showXAxis: false, + showYAxis: false, + useInteractiveGuideline: true, + tooltips: true, + pointSize: 0, + color: ['#444', '#777', '#aaa'], + margin: { + top: 10, + left: 0, + right: 0, + bottom: 20 + }, + xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } }, + yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, }, + y: function (d) { return d.y; }, + x: function (d) { return d.x; } + } + }; +}; diff --git a/src/plugins/status_page/public/lib/read_stat_data.js b/src/plugins/status_page/public/lib/read_stat_data.js new file mode 100644 index 0000000000000..7c1fa8b0e9f84 --- /dev/null +++ b/src/plugins/status_page/public/lib/read_stat_data.js @@ -0,0 +1,39 @@ +import _ from 'lodash'; + +module.exports = function readStatData(data, seriesNames) { + // Metric Values format + // metric: [[xValue, yValue], ...] + // LoadMetric: + // metric: [[xValue, [yValue, yValue2, yValue3]], ...] + // return [ + // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]}, + // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]}, + // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}] + // + // Go through all of the metric values and split the values out. + // returns an array of all of the averages + + const metricList = []; + seriesNames = seriesNames || []; + data.forEach(function (vector) { + vector = _.flatten(vector); + const x = vector.shift(); + vector.forEach(function (yValue, i) { + const series = seriesNames[i] || ''; + + if (!metricList[i]) { + metricList[i] = { + key: series, + values: [] + }; + } + // unshift to make sure they're in the correct order + metricList[i].values.unshift({ + x: x, + y: yValue + }); + }); + }); + + return metricList; +}; diff --git a/src/plugins/status_page/public/lib/to_title_case.js b/src/plugins/status_page/public/lib/to_title_case.js new file mode 100644 index 0000000000000..94e230cc3fef8 --- /dev/null +++ b/src/plugins/status_page/public/lib/to_title_case.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; + +// Turns thisIsASentence to +// This Is A Sentence +module.exports = function toTitleCase(name) { + return name + .split(/(?=[A-Z])/) + .map(function (word) { return word[0].toUpperCase() + _.rest(word).join(''); }) + .join(' '); +}; diff --git a/src/plugins/status_page/public/status_page.html b/src/plugins/status_page/public/status_page.html new file mode 100644 index 0000000000000..09ef885ac32c4 --- /dev/null +++ b/src/plugins/status_page/public/status_page.html @@ -0,0 +1,44 @@ +
    +
    +

    + Status: {{ ui.serverStateMessage }} + + + {{ ui.name }} + +

    +
    + +
    +
    + +
    +
    + +
    +

    Installed Plugins

    +
    + +
    + +

    + No plugin status information available +

    + + + + + + + + + + + + +
    NameVersionStatus
    {{status.name}}{{status.version}} + + {{status.message}} +
    +
    +
    diff --git a/src/plugins/status_page/public/status_page.js b/src/plugins/status_page/public/status_page.js new file mode 100644 index 0000000000000..2fc65f299b803 --- /dev/null +++ b/src/plugins/status_page/public/status_page.js @@ -0,0 +1,56 @@ +import _ from 'lodash'; +import notify from 'ui/notify'; +import $ from 'jquery'; +import 'ui/autoload/styles'; +import 'plugins/status_page/status_page_metric'; +import 'plugins/status_page/status_page.less'; +import uiModules from 'ui/modules'; + + +const chrome = require('ui/chrome') +.setRootTemplate(require('plugins/status_page/status_page.html')) +.setRootController('ui', function ($http, $scope) { + const ui = this; + ui.loading = false; + + ui.refresh = function () { + ui.loading = true; + + // go ahead and get the info you want + return $http + .get(chrome.addBasePath('/api/status')) + .then(function (resp) { + + if (ui.fetchError) { + ui.fetchError.clear(); + ui.fetchError = null; + } + + const data = resp.data; + ui.metrics = data.metrics; + ui.statuses = data.status.statuses; + ui.name = data.name; + + const overall = data.status.overall; + if (!ui.serverState || (ui.serverState !== overall.state)) { + ui.serverState = overall.state; + ui.serverStateMessage = overall.title; + } + }) + .catch(function () { + if (ui.fetchError) return; + ui.fetchError = notify.error('Failed to request server ui. Perhaps your server is down?'); + ui.metrics = ui.statuses = ui.overall = null; + }) + .then(function () { + ui.loading = false; + }); + }; + + ui.refresh(); +}); + +uiModules.get('kibana') +.config(function (appSwitcherEnsureNavigationProvider) { + appSwitcherEnsureNavigationProvider.forceNavigation(true); +}); diff --git a/src/plugins/statusPage/public/statusPage.less b/src/plugins/status_page/public/status_page.less similarity index 100% rename from src/plugins/statusPage/public/statusPage.less rename to src/plugins/status_page/public/status_page.less diff --git a/src/plugins/statusPage/public/statusPageMetric.html b/src/plugins/status_page/public/status_page_metric.html similarity index 100% rename from src/plugins/statusPage/public/statusPageMetric.html rename to src/plugins/status_page/public/status_page_metric.html diff --git a/src/plugins/status_page/public/status_page_metric.js b/src/plugins/status_page/public/status_page_metric.js new file mode 100644 index 0000000000000..1893e8eda2d4b --- /dev/null +++ b/src/plugins/status_page/public/status_page_metric.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import moment from 'moment'; +import numeral from 'numeral'; + +import toTitleCase from './lib/to_title_case'; +import formatNumber from './lib/format_number'; +import readStatData from './lib/read_stat_data'; +import uiModules from 'ui/modules'; +import statusPageMetricTemplate from 'plugins/status_page/status_page_metric.html'; + +function calcAvg(metricList, metricNumberType) { + return metricList.map(function (data) { + const uglySum = data.values.reduce(function (sumSoFar, vector) { + return sumSoFar + vector.y; + }, 0); + return formatNumber(uglySum / data.values.length, metricNumberType); + }); +} + +uiModules +.get('kibana', []) +.directive('statusPageMetric', function () { + return { + restrict: 'E', + template: statusPageMetricTemplate, + scope: { + name: '@', + data: '=' + }, + controllerAs: 'metric', + controller: function ($scope) { + const self = this; + + self.name = $scope.name; + self.title = toTitleCase(self.name); + self.extendedTitle = self.title; + self.numberType = 'precise'; + self.seriesNames = []; + + switch (self.name) { + case 'heapTotal': + case 'heapUsed': + self.numberType = 'byte'; + break; + + case 'responseTimeAvg': + case 'responseTimeMax': + self.numberType = 'ms'; + break; + + case 'load': + self.seriesNames = ['1min', '5min', '15min']; + break; + } + + $scope.$watch('data', function (data) { + self.rawData = data; + self.chartData = readStatData(self.rawData, self.seriesNames); + self.averages = calcAvg(self.chartData, self.numberType); + + let unit = ''; + self.averages = self.averages.map(function (average) { + const parts = average.split(' '); + const value = parts.shift(); + unit = parts.join(' '); + return value; + }); + self.extendedTitle = self.title; + if (unit) { + self.extendedTitle = `${self.extendedTitle} (${unit})`; + } + }); + } + }; +}); diff --git a/src/plugins/table_vis/index.js b/src/plugins/table_vis/index.js index 438e8cee479cf..88b6f81386311 100644 --- a/src/plugins/table_vis/index.js +++ b/src/plugins/table_vis/index.js @@ -1,4 +1,4 @@ -module.exports = function (kibana) { +export default function (kibana) { return new kibana.Plugin({ uiExports: { diff --git a/src/plugins/table_vis/public/__tests__/_table_vis.js b/src/plugins/table_vis/public/__tests__/_table_vis.js index e2068932f61cc..8578d7ce1529b 100644 --- a/src/plugins/table_vis/public/__tests__/_table_vis.js +++ b/src/plugins/table_vis/public/__tests__/_table_vis.js @@ -1,27 +1,30 @@ +import $ from 'jquery'; +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'auto-release-sinon'; +import AggResponseTabifyTableGroupProvider from 'ui/agg_response/tabify/_table_group'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('Integration', function () { - var $ = require('jquery'); - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var sinon = require('auto-release-sinon'); - - var $rootScope; - var TableGroup; - var $compile; - var $scope; - var $el; - var Vis; - var indexPattern; - var fixtures; + + let $rootScope; + let TableGroup; + let $compile; + let $scope; + let $el; + let Vis; + let indexPattern; + let fixtures; beforeEach(ngMock.module('kibana', 'kibana/table_vis')); beforeEach(ngMock.inject(function (Private, $injector) { $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); fixtures = require('fixtures/fake_hierarchical_data'); - TableGroup = Private(require('ui/agg_response/tabify/_table_group')); - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + TableGroup = Private(AggResponseTabifyTableGroupProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); // basically a parameterized beforeEach @@ -30,7 +33,8 @@ describe('Integration', function () { $rootScope.vis = vis; $rootScope.esResponse = esResponse; - $el = $(''); + $rootScope.uiState = require('fixtures/mock_ui_state'); + $el = $(''); $compile($el)($rootScope); $rootScope.$apply(); @@ -92,7 +96,7 @@ describe('Integration', function () { it('passes the table groups to the kbnAggTableGroup directive', function () { init(new OneRangeVis(), fixtures.oneRangeBucket); - var $atg = $el.find('kbn-agg-table-group').first(); + const $atg = $el.find('kbn-agg-table-group').first(); expect($atg.size()).to.be(1); expect($atg.attr('group')).to.be('tableGroups'); expect($atg.isolateScope().group).to.be($atg.scope().tableGroups); @@ -103,18 +107,18 @@ describe('Integration', function () { expect($el.find('kbn-agg-table-group').size()).to.be(0); - var $err = $el.find('.table-vis-error'); + const $err = $el.find('.table-vis-error'); expect($err.size()).to.be(1); expect($err.text().trim()).to.be('No results found'); }); it('displays an error if the search hits, but didn\'t create any rows', function () { - var visParams = { + const visParams = { showPartialRows: false, metricsAtAllLevels: true }; - var resp = _.cloneDeep(fixtures.threeTermBuckets); + const resp = _.cloneDeep(fixtures.threeTermBuckets); resp.aggregations.agg_2.buckets.forEach(function (extensionBucket) { extensionBucket.agg_3.buckets.forEach(function (countryBucket) { // clear all the machine os buckets @@ -126,7 +130,7 @@ describe('Integration', function () { expect($el.find('kbn-agg-table-group').size()).to.be(0); - var $err = $el.find('.table-vis-error'); + const $err = $el.find('.table-vis-error'); expect($err.size()).to.be(1); expect($err.text().trim()).to.be('No results found'); }); diff --git a/src/plugins/table_vis/public/__tests__/_table_vis_controller.js b/src/plugins/table_vis/public/__tests__/_table_vis_controller.js index c7f0575b1c9ee..8941be2b1b54c 100644 --- a/src/plugins/table_vis/public/__tests__/_table_vis_controller.js +++ b/src/plugins/table_vis/public/__tests__/_table_vis_controller.js @@ -1,18 +1,24 @@ +import $ from 'jquery'; +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'auto-release-sinon'; +import tabifyPm from 'ui/agg_response/tabify/tabify'; +import AggResponseTabifyTableGroupProvider from 'ui/agg_response/tabify/_table_group'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import StateManagementAppStateProvider from 'ui/state_management/app_state'; describe('Controller', function () { - var $ = require('jquery'); - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var sinon = require('auto-release-sinon'); - - var $rootScope; - var TableGroup; - var $compile; - var Private; - var $scope; - var $el; - var Vis; - var fixtures; + + let $rootScope; + let TableGroup; + let $compile; + let Private; + let $scope; + let $el; + let Vis; + let fixtures; + let AppState; beforeEach(ngMock.module('kibana', 'kibana/table_vis')); beforeEach(ngMock.inject(function ($injector) { @@ -20,13 +26,14 @@ describe('Controller', function () { $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); fixtures = require('fixtures/fake_hierarchical_data'); - TableGroup = Private(require('ui/agg_response/tabify/_table_group')); - Vis = Private(require('ui/Vis')); + AppState = Private(StateManagementAppStateProvider); + TableGroup = Private(AggResponseTabifyTableGroupProvider); + Vis = Private(VisProvider); })); function OneRangeVis(params) { return new Vis( - Private(require('fixtures/stubbed_logstash_index_pattern')), + Private(FixturesStubbedLogstashIndexPatternProvider), { type: 'table', params: params || {}, @@ -53,6 +60,7 @@ describe('Controller', function () { vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); $rootScope.vis = vis; + $rootScope.uiState = new AppState({uiState: {}}).makeStateful('uiState'); $rootScope.newScope = function (scope) { $scope = scope; }; $el = $('
    ') @@ -99,11 +107,28 @@ describe('Controller', function () { expect(!$scope.tableGroups).to.be.ok(); }); + it('sets the sort on the scope when it is passed as a vis param', function () { + const sortObj = { + columnIndex: 1, + direction: 'asc' + }; + initController(new OneRangeVis({sort: sortObj})); + + // modify the data to not have any buckets + const resp = _.cloneDeep(fixtures.oneRangeBucket); + resp.aggregations.agg_2.buckets = {}; + + attachEsResponseToScope(resp); + + expect($scope.sort.columnIndex).to.equal(sortObj.columnIndex); + expect($scope.sort.direction).to.equal(sortObj.direction); + }); + it('sets #hasSomeRows properly if the table group is empty', function () { initController(new OneRangeVis()); // modify the data to not have any buckets - var resp = _.cloneDeep(fixtures.oneRangeBucket); + const resp = _.cloneDeep(fixtures.oneRangeBucket); resp.aggregations.agg_2.buckets = {}; attachEsResponseToScope(resp); @@ -114,11 +139,10 @@ describe('Controller', function () { it('passes partialRows:true to tabify based on the vis params', function () { // spy on the tabify private module - var tabifyPm = require('ui/agg_response/tabify/tabify'); - var spiedTabify = sinon.spy(Private(tabifyPm)); + const spiedTabify = sinon.spy(Private(tabifyPm)); Private.stub(tabifyPm, spiedTabify); - var vis = new OneRangeVis({ showPartialRows: true }); + const vis = new OneRangeVis({ showPartialRows: true }); initController(vis); attachEsResponseToScope(fixtures.oneRangeBucket); @@ -128,11 +152,10 @@ describe('Controller', function () { it('passes partialRows:false to tabify based on the vis params', function () { // spy on the tabify private module - var tabifyPm = require('ui/agg_response/tabify/tabify'); - var spiedTabify = sinon.spy(Private(tabifyPm)); + const spiedTabify = sinon.spy(Private(tabifyPm)); Private.stub(tabifyPm, spiedTabify); - var vis = new OneRangeVis({ showPartialRows: false }); + const vis = new OneRangeVis({ showPartialRows: false }); initController(vis); attachEsResponseToScope(fixtures.oneRangeBucket); @@ -142,11 +165,10 @@ describe('Controller', function () { it('passes partialRows:true to tabify based on the vis params', function () { // spy on the tabify private module - var tabifyPm = require('ui/agg_response/tabify/tabify'); - var spiedTabify = sinon.spy(Private(tabifyPm)); + const spiedTabify = sinon.spy(Private(tabifyPm)); Private.stub(tabifyPm, spiedTabify); - var vis = new OneRangeVis({ showPartialRows: true }); + const vis = new OneRangeVis({ showPartialRows: true }); initController(vis); attachEsResponseToScope(fixtures.oneRangeBucket); @@ -156,11 +178,10 @@ describe('Controller', function () { it('passes partialRows:false to tabify based on the vis params', function () { // spy on the tabify private module - var tabifyPm = require('ui/agg_response/tabify/tabify'); - var spiedTabify = sinon.spy(Private(tabifyPm)); + const spiedTabify = sinon.spy(Private(tabifyPm)); Private.stub(tabifyPm, spiedTabify); - var vis = new OneRangeVis({ showPartialRows: false }); + const vis = new OneRangeVis({ showPartialRows: false }); initController(vis); attachEsResponseToScope(fixtures.oneRangeBucket); diff --git a/src/plugins/table_vis/public/__tests__/index.js b/src/plugins/table_vis/public/__tests__/index.js index c1bef98f8f93f..15d3519721346 100644 --- a/src/plugins/table_vis/public/__tests__/index.js +++ b/src/plugins/table_vis/public/__tests__/index.js @@ -1,4 +1,4 @@ +import './_table_vis_controller'; +import './_table_vis'; describe('Table Vis', function () { - require('./_table_vis_controller'); - require('./_table_vis'); }); diff --git a/src/plugins/table_vis/public/table_vis.html b/src/plugins/table_vis/public/table_vis.html index 68e604ef8fe16..33fdf9cf5e8f1 100644 --- a/src/plugins/table_vis/public/table_vis.html +++ b/src/plugins/table_vis/public/table_vis.html @@ -5,6 +5,11 @@

    No results found

    - + +
    -
    \ No newline at end of file +
    diff --git a/src/plugins/table_vis/public/table_vis.js b/src/plugins/table_vis/public/table_vis.js index a912925950f6c..91a6d00da2668 100644 --- a/src/plugins/table_vis/public/table_vis.js +++ b/src/plugins/table_vis/public/table_vis.js @@ -1,71 +1,76 @@ -define(function (require) { - // we need to load the css ourselves - require('plugins/table_vis/table_vis.less'); +import 'plugins/table_vis/table_vis.less'; +import 'plugins/table_vis/table_vis_controller'; +import 'plugins/table_vis/table_vis_params'; +import 'ui/agg_table'; +import 'ui/agg_table/agg_table_group'; +import TemplateVisTypeTemplateVisTypeProvider from 'ui/template_vis_type/template_vis_type'; +import VisSchemasProvider from 'ui/vis/schemas'; +import tableVisTemplate from 'plugins/table_vis/table_vis.html'; +// we need to load the css ourselves - // we also need to load the controller and used by the template - require('plugins/table_vis/table_vis_controller'); +// we also need to load the controller and used by the template - // our params are a bit complex so we will manage them with a directive - require('plugins/table_vis/table_vis_params'); +// our params are a bit complex so we will manage them with a directive - // require the directives that we use as well - require('ui/agg_table'); - require('ui/agg_table/agg_table_group'); +// require the directives that we use as well - // register the provider with the visTypes registry - require('ui/registry/vis_types').register(TableVisTypeProvider); +// register the provider with the visTypes registry +require('ui/registry/vis_types').register(TableVisTypeProvider); - // define the TableVisType - function TableVisTypeProvider(Private) { - var TemplateVisType = Private(require('ui/template_vis_type/TemplateVisType')); - var Schemas = Private(require('ui/Vis/Schemas')); +// define the TableVisType +function TableVisTypeProvider(Private) { + const TemplateVisType = Private(TemplateVisTypeTemplateVisTypeProvider); + const Schemas = Private(VisSchemasProvider); - // define the TableVisController which is used in the template - // by angular's ng-controller directive + // define the TableVisController which is used in the template + // by angular's ng-controller directive - // return the visType object, which kibana will use to display and configure new - // Vis object of this type. - return new TemplateVisType({ - name: 'table', - title: 'Data table', - icon: 'fa-table', - description: 'The data table provides a detailed breakdown, in tabular format, of the results of a composed ' + - 'aggregation. Tip, a data table is available from many other charts by clicking grey bar at the bottom of the chart.', - template: require('plugins/table_vis/table_vis.html'), - params: { - defaults: { - perPage: 10, - showPartialRows: false, - showMeticsAtAllLevels: false - }, - editor: '' + // return the visType object, which kibana will use to display and configure new + // Vis object of this type. + return new TemplateVisType({ + name: 'table', + title: 'Data table', + icon: 'fa-table', + description: 'The data table provides a detailed breakdown, in tabular format, of the results of a composed ' + + 'aggregation. Tip, a data table is available from many other charts by clicking grey bar at the bottom of the chart.', + template: tableVisTemplate, + params: { + defaults: { + perPage: 10, + showPartialRows: false, + showMeticsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null + } }, - hierarchicalData: function (vis) { - return Boolean(vis.params.showPartialRows || vis.params.showMeticsAtAllLevels); + editor: '' + }, + hierarchicalData: function (vis) { + return Boolean(vis.params.showPartialRows || vis.params.showMeticsAtAllLevels); + }, + schemas: new Schemas([ + { + group: 'metrics', + name: 'metric', + title: 'Metric', + min: 1, + defaults: [ + { type: 'count', schema: 'metric' } + ] }, - schemas: new Schemas([ - { - group: 'metrics', - name: 'metric', - title: 'Metric', - min: 1, - defaults: [ - { type: 'count', schema: 'metric' } - ] - }, - { - group: 'buckets', - name: 'bucket', - title: 'Split Rows' - }, - { - group: 'buckets', - name: 'split', - title: 'Split Table' - } - ]) - }); - } + { + group: 'buckets', + name: 'bucket', + title: 'Split Rows' + }, + { + group: 'buckets', + name: 'split', + title: 'Split Table' + } + ]) + }); +} - return TableVisTypeProvider; -}); +export default TableVisTypeProvider; diff --git a/src/plugins/table_vis/public/table_vis_controller.js b/src/plugins/table_vis/public/table_vis_controller.js index 970e82f025d97..1d7af7d3c1b7e 100644 --- a/src/plugins/table_vis/public/table_vis_controller.js +++ b/src/plugins/table_vis/public/table_vis_controller.js @@ -1,39 +1,48 @@ -define(function (require) { - // get the kibana/table_vis module, and make sure that it requires the "kibana" module if it - // didn't already - var module = require('ui/modules').get('kibana/table_vis', ['kibana']); - - // add a controller to tha module, which will transform the esResponse into a - // tabular format that we can pass to the table directive - module.controller('KbnTableVisController', function ($scope, Private) { - var tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); - - $scope.$watch('esResponse', function (resp, oldResp) { - var tableGroups = $scope.tableGroups = null; - var hasSomeRows = $scope.hasSomeRows = null; - - if (resp) { - var vis = $scope.vis; - var params = vis.params; - - tableGroups = tabifyAggResponse(vis, resp, { - partialRows: params.showPartialRows, - minimalColumns: vis.isHierarchical() && !params.showMeticsAtAllLevels, - asAggConfigResults: true - }); - - hasSomeRows = tableGroups.tables.some(function haveRows(table) { - if (table.tables) return table.tables.some(haveRows); - return table.rows.length > 0; - }); - } - - $scope.hasSomeRows = hasSomeRows; - if (hasSomeRows) { - $scope.tableGroups = tableGroups; - } - - }); +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import uiModules from 'ui/modules'; +import { assign } from 'lodash'; + +// get the kibana/table_vis module, and make sure that it requires the "kibana" module if it +// didn't already +const module = uiModules.get('kibana/table_vis', ['kibana']); + +// add a controller to tha module, which will transform the esResponse into a +// tabular format that we can pass to the table directive +module.controller('KbnTableVisController', function ($scope, Private) { + const tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); + + var uiStateSort = ($scope.uiState) ? $scope.uiState.get('vis.params.sort') : {}; + assign($scope.vis.params.sort, uiStateSort); + + $scope.sort = $scope.vis.params.sort; + $scope.$watchCollection('sort', function (newSort) { + $scope.uiState.set('vis.params.sort', newSort); }); + $scope.$watch('esResponse', function (resp, oldResp) { + let tableGroups = $scope.tableGroups = null; + let hasSomeRows = $scope.hasSomeRows = null; + + if (resp) { + const vis = $scope.vis; + const params = vis.params; + + tableGroups = tabifyAggResponse(vis, resp, { + partialRows: params.showPartialRows, + minimalColumns: vis.isHierarchical() && !params.showMeticsAtAllLevels, + asAggConfigResults: true + }); + + hasSomeRows = tableGroups.tables.some(function haveRows(table) { + if (table.tables) return table.tables.some(haveRows); + return table.rows.length > 0; + }); + } + + $scope.hasSomeRows = hasSomeRows; + if (hasSomeRows) { + $scope.tableGroups = tableGroups; + } + }); }); + diff --git a/src/plugins/table_vis/public/table_vis_params.js b/src/plugins/table_vis/public/table_vis_params.js index 0d8f5f7ae43d9..3b1e703ede435 100644 --- a/src/plugins/table_vis/public/table_vis_params.js +++ b/src/plugins/table_vis/public/table_vis_params.js @@ -1,26 +1,26 @@ -define(function (require) { - var _ = require('lodash'); +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import tableVisParamsTemplate from 'plugins/table_vis/table_vis_params.html'; - require('ui/modules').get('kibana/table_vis') - .directive('tableVisParams', function () { - return { - restrict: 'E', - template: require('plugins/table_vis/table_vis_params.html'), - link: function ($scope) { - $scope.$watchMulti([ - 'vis.params.showPartialRows', - 'vis.params.showMeticsAtAllLevels' - ], function () { - if (!$scope.vis) return; +uiModules.get('kibana/table_vis') +.directive('tableVisParams', function () { + return { + restrict: 'E', + template: tableVisParamsTemplate, + link: function ($scope) { + $scope.$watchMulti([ + 'vis.params.showPartialRows', + 'vis.params.showMeticsAtAllLevels' + ], function () { + if (!$scope.vis) return; - var params = $scope.vis.params; - if (params.showPartialRows || params.showMeticsAtAllLevels) { - $scope.metricsAtAllLevels = true; - } else { - $scope.metricsAtAllLevels = false; - } - }); - } - }; - }); + const params = $scope.vis.params; + if (params.showPartialRows || params.showMeticsAtAllLevels) { + $scope.metricsAtAllLevels = true; + } else { + $scope.metricsAtAllLevels = false; + } + }); + } + }; }); diff --git a/src/plugins/testsBundle/findSourceFiles.js b/src/plugins/testsBundle/findSourceFiles.js deleted file mode 100644 index fa65cf02a19db..0000000000000 --- a/src/plugins/testsBundle/findSourceFiles.js +++ /dev/null @@ -1,40 +0,0 @@ -let { chain, memoize } = require('lodash'); -let { resolve } = require('path'); -let { map, fromNode } = require('bluebird'); -let { Glob } = require('glob'); - -let fromRoot = require('../../utils/fromRoot'); - -let findSourceFiles = async (patterns, cwd = fromRoot('.')) => { - patterns = [].concat(patterns || []); - - let matcheses = await map(patterns, async pattern => { - return await fromNode(cb => { - let g = new Glob(pattern, { - cwd: cwd, - ignore: [ - 'node_modules/**/*', - 'bower_components/**/*', - '**/_*.js' - ], - symlinks: findSourceFiles.symlinks, - statCache: findSourceFiles.statCache, - realpathCache: findSourceFiles.realpathCache, - cache: findSourceFiles.cache - }, cb); - }); - }); - - return chain(matcheses) - .flatten() - .uniq() - .map(match => resolve(cwd, match)) - .value(); -}; - -findSourceFiles.symlinks = {}; -findSourceFiles.statCache = {}; -findSourceFiles.realpathCache = {}; -findSourceFiles.cache = {}; - -module.exports = findSourceFiles; diff --git a/src/plugins/testsBundle/index.js b/src/plugins/testsBundle/index.js deleted file mode 100644 index f0f3923804a5c..0000000000000 --- a/src/plugins/testsBundle/index.js +++ /dev/null @@ -1,60 +0,0 @@ -module.exports = (kibana) => { - let { union } = require('lodash'); - - let utils = require('requirefrom')('src/utils'); - let fromRoot = utils('fromRoot'); - let findSourceFiles = require('./findSourceFiles'); - - return new kibana.Plugin({ - config: (Joi) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - instrument: Joi.boolean().default(false) - }).default(); - }, - - uiExports: { - bundle: async (UiBundle, env, apps) => { - let modules = []; - let config = kibana.config; - - // add the modules from all of the apps - for (let app of apps) { - modules = union(modules, app.getModules()); - } - - let testFiles = await findSourceFiles([ - 'src/**/public/**/__tests__/**/*.js', - 'installedPlugins/*/public/**/__tests__/**/*.js' - ]); - - for (let f of testFiles) modules.push(f); - - if (config.get('testsBundle.instrument')) { - env.addPostLoader({ - test: /\.jsx?$/, - exclude: /[\/\\](__tests__|node_modules|bower_components|webpackShims)[\/\\]/, - loader: 'istanbul-instrumenter' - }); - } - - return new UiBundle({ - id: 'tests', - modules: modules, - template: require('./testsEntryTemplate'), - env: env - }); - }, - - modules: { - ngMock$: fromRoot('src/plugins/devMode/public/ngMock'), - fixtures: fromRoot('src/fixtures'), - testUtils: fromRoot('src/testUtils'), - 'angular-mocks': { - path: require.resolve('angular-mocks'), - imports: 'angular' - }, - } - } - }); -}; diff --git a/src/plugins/testsBundle/package.json b/src/plugins/testsBundle/package.json deleted file mode 100644 index 0b7104a0c3f34..0000000000000 --- a/src/plugins/testsBundle/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "testsBundle", - "version": "0.0.0" -} diff --git a/src/plugins/testsBundle/testsEntryTemplate.js b/src/plugins/testsBundle/testsEntryTemplate.js deleted file mode 100644 index aff1601d50675..0000000000000 --- a/src/plugins/testsBundle/testsEntryTemplate.js +++ /dev/null @@ -1,39 +0,0 @@ -module.exports = function ({env, bundle}) { - - let pluginSlug = env.pluginInfo.sort() - .map(p => ' * - ' + p) - .join('\n'); - - let requires = bundle.modules - .map(m => `require(${JSON.stringify(m)});`) - .join('\n'); - - return ` -/** - * Test entry file - * - * This is programatically created and updated, do not modify - * - * context: ${JSON.stringify(env.context)} - * includes code from: -${pluginSlug} - * - */ - -window.__KBN__ = { - version: '1.2.3', - buildNum: 1234, - vars: { - kbnIndex: '.kibana', - esShardTimeout: 1500, - esApiVersion: '2.0', - } -}; - -require('ui/testHarness'); -${requires} -require('ui/testHarness').bootstrap(/* go! */); - -`; - -}; diff --git a/src/plugins/tests_bundle/find_source_files.js b/src/plugins/tests_bundle/find_source_files.js new file mode 100644 index 0000000000000..c6b6151166cda --- /dev/null +++ b/src/plugins/tests_bundle/find_source_files.js @@ -0,0 +1,38 @@ + +import { fromRoot } from '../../utils'; +import { chain, memoize } from 'lodash'; +import { resolve } from 'path'; +import { map, fromNode } from 'bluebird'; +import glob from 'glob-all'; + +let findSourceFiles = async (patterns, cwd = fromRoot('.')) => { + patterns = [].concat(patterns || []); + + const matches = await fromNode(cb => { + glob(patterns, { + cwd: cwd, + ignore: [ + 'node_modules/**/*', + 'bower_components/**/*', + '**/_*.js' + ], + symlinks: findSourceFiles.symlinks, + statCache: findSourceFiles.statCache, + realpathCache: findSourceFiles.realpathCache, + cache: findSourceFiles.cache + }, cb); + }); + + return chain(matches) + .flatten() + .uniq() + .map(match => resolve(cwd, match)) + .value(); +}; + +findSourceFiles.symlinks = {}; +findSourceFiles.statCache = {}; +findSourceFiles.realpathCache = {}; +findSourceFiles.cache = {}; + +module.exports = findSourceFiles; diff --git a/src/plugins/tests_bundle/index.js b/src/plugins/tests_bundle/index.js new file mode 100644 index 0000000000000..db7cc5474d173 --- /dev/null +++ b/src/plugins/tests_bundle/index.js @@ -0,0 +1,81 @@ +import { union } from 'lodash'; +import defaultsProvider from '../../ui/settings/defaults'; +import findSourceFiles from './find_source_files'; +import { fromRoot } from '../../utils'; + +export default (kibana) => { + return new kibana.Plugin({ + config: (Joi) => { + return Joi.object({ + enabled: Joi.boolean().default(true), + instrument: Joi.boolean().default(false), + pluginId: Joi.string() + }).default(); + }, + + uiExports: { + bundle: async (UiBundle, env, apps, plugins) => { + let modules = []; + let config = kibana.config; + + const testGlobs = ['src/ui/public/**/*.js']; + const testingPluginIds = config.get('tests_bundle.pluginId'); + + if (testingPluginIds) { + testGlobs.push('!src/ui/public/**/__tests__/**/*'); + testingPluginIds.split(',').forEach((pluginId) => { + const plugin = plugins.byId[pluginId]; + if (!plugin) throw new Error('Invalid testingPluginId :: unknown plugin ' + pluginId); + + // add the modules from all of this plugins apps + for (let app of plugin.apps) { + modules = union(modules, app.getModules()); + } + + testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); + }); + } else { + + // add the modules from all of the apps + for (let app of apps) { + modules = union(modules, app.getModules()); + } + + for (const plugin of plugins) { + testGlobs.push(`${plugin.publicDir}/**/__tests__/**/*.js`); + } + } + + const testFiles = await findSourceFiles(testGlobs); + for (let f of testFiles) modules.push(f); + + if (config.get('tests_bundle.instrument')) { + env.addPostLoader({ + test: /\.jsx?$/, + exclude: /[\/\\](__tests__|node_modules|bower_components|webpackShims)[\/\\]/, + loader: 'istanbul-instrumenter' + }); + } + + env.defaultUiSettings = defaultsProvider(); + + return new UiBundle({ + id: 'tests', + modules: modules, + template: require('./tests_entry_template'), + env: env + }); + }, + + modules: { + ng_mock$: fromRoot('src/plugins/dev_mode/public/ng_mock'), + fixtures: fromRoot('src/fixtures'), + test_utils: fromRoot('src/test_utils'), + 'angular-mocks': { + path: require.resolve('angular-mocks'), + imports: 'angular' + }, + } + } + }); +}; diff --git a/src/plugins/tests_bundle/package.json b/src/plugins/tests_bundle/package.json new file mode 100644 index 0000000000000..dd351b09560c4 --- /dev/null +++ b/src/plugins/tests_bundle/package.json @@ -0,0 +1,4 @@ +{ + "name": "tests_bundle", + "version": "0.0.0" +} diff --git a/src/plugins/tests_bundle/tests_entry_template.js b/src/plugins/tests_bundle/tests_entry_template.js new file mode 100644 index 0000000000000..79645defda328 --- /dev/null +++ b/src/plugins/tests_bundle/tests_entry_template.js @@ -0,0 +1,43 @@ +module.exports = function ({env, bundle}) { + + const pluginSlug = env.pluginInfo.sort() + .map(p => ' * - ' + p) + .join('\n'); + + const requires = bundle.modules + .map(m => `require(${JSON.stringify(m)});`) + .join('\n'); + + return ` +/** + * Test entry file + * + * This is programatically created and updated, do not modify + * + * context: ${JSON.stringify(env.context)} + * includes code from: +${pluginSlug} + * + */ + +window.__KBN__ = { + version: '1.2.3', + buildNum: 1234, + vars: { + kbnIndex: '.kibana', + esShardTimeout: 1500, + esApiVersion: '2.0', + esRequestTimeout: '300000' + }, + uiSettings: { + defaults: ${JSON.stringify(env.defaultUiSettings, null, 2).split('\n').join('\n ')}, + user: {} + } +}; + +require('ui/test_harness'); +${requires} +require('ui/test_harness').bootstrap(/* go! */); +`; + +}; diff --git a/src/server/KbnServer.js b/src/server/KbnServer.js deleted file mode 100644 index 3bbbca675ae01..0000000000000 --- a/src/server/KbnServer.js +++ /dev/null @@ -1,100 +0,0 @@ -let { constant, once, compact, flatten } = require('lodash'); -let { promisify, resolve, fromNode } = require('bluebird'); -let Hapi = require('hapi'); - -let utils = require('requirefrom')('src/utils'); -let rootDir = utils('fromRoot')('.'); -let pkg = utils('packageJson'); - -module.exports = class KbnServer { - constructor(settings) { - this.name = pkg.name; - this.version = pkg.version; - this.build = pkg.build || false; - this.rootDir = rootDir; - this.settings = settings || {}; - - this.ready = constant(this.mixin( - require('./config/setup'), // sets this.config, reads this.settings - require('./http'), // sets this.server - require('./logging'), - require('./status'), - - // find plugins and set this.plugins - require('./plugins/scan'), - - // tell the config we are done loading plugins - require('./config/complete'), - - // setup this.uiExports and this.bundles - require('../ui'), - - // ensure that all bundles are built, or that the - // lazy bundle server is running - require('../optimize'), - - // finally, initialize the plugins - require('./plugins/initialize'), - - () => { - if (this.config.get('server.autoListen')) { - this.ready = constant(resolve()); - return this.listen(); - } - } - )); - - this.listen = once(this.listen); - } - - /** - * Extend the KbnServer outside of the constraits of a plugin. This allows access - * to APIs that are not exposed (intentionally) to the plugins and should only - * be used when the code will be kept up to date with Kibana. - * - * @param {...function} - functions that should be called to mixin functionality. - * They are called with the arguments (kibana, server, config) - * and can return a promise to delay execution of the next mixin - * @return {Promise} - promise that is resolved when the final mixin completes. - */ - async mixin(...fns) { - for (let fn of compact(flatten(fns))) { - await fn.call(this, this, this.server, this.config); - } - } - - /** - * Tell the server to listen for incoming requests, or get - * a promise that will be resolved once the server is listening. - * - * @return undefined - */ - async listen() { - let { server, config } = this; - - await this.ready(); - await fromNode(cb => server.start(cb)); - await require('./pid')(this, server, config); - - server.log(['listening', 'info'], 'Server running at ' + server.info.uri); - return server; - } - - async close() { - await fromNode(cb => this.server.stop(cb)); - } - - async inject(opts) { - if (!this.server) await this.ready(); - - return await fromNode(cb => { - try { - this.server.inject(opts, (resp) => { - cb(null, resp); - }); - } catch (err) { - cb(err); - } - }); - } -}; diff --git a/src/server/__tests__/basePath.js b/src/server/__tests__/basePath.js deleted file mode 100644 index e9b362df40be9..0000000000000 --- a/src/server/__tests__/basePath.js +++ /dev/null @@ -1,38 +0,0 @@ -import { resolve } from 'path'; -import { fromNode as fn } from 'bluebird'; -import expect from 'expect.js'; - -import KbnServer from '../KbnServer'; - -const src = resolve.bind(__dirname, '../../'); -const basePath = '/kibana'; - -describe('Server basePath config', function () { - this.slow(10000); - this.timeout(60000); - - let kbnServer; - before(async function () { - kbnServer = new KbnServer({ - server: { autoListen: false, basePath }, - plugins: { scanDirs: [src('plugins')] }, - logging: { quiet: true }, - optimize: { enabled: false }, - }); - await kbnServer.ready(); - return kbnServer; - }); - - after(async function () { - await kbnServer.close(); - }); - - it('appends the basePath to root redirect', async function () { - const response = await kbnServer.inject({ - url: '/', - method: 'GET' - }); - - expect(response.payload).to.match(/defaultRoute = '\/kibana\/app\/kibana'/); - }); -}); diff --git a/src/server/__tests__/base_path.js b/src/server/__tests__/base_path.js new file mode 100644 index 0000000000000..fde7ec98f128a --- /dev/null +++ b/src/server/__tests__/base_path.js @@ -0,0 +1,39 @@ +import { resolve } from 'path'; +import { fromNode as fn } from 'bluebird'; +import expect from 'expect.js'; + +import * as kbnTestServer from '../../../test/utils/kbn_server'; +const basePath = '/kibana'; + +describe('Server basePath config', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + before(async function () { + kbnServer = kbnTestServer.createServer({ + server: { basePath } + }); + await kbnServer.ready(); + return kbnServer; + }); + + after(async function () { + await kbnServer.close(); + }); + + it('appends the basePath to root redirect', function (done) { + const options = { + url: '/', + method: 'GET' + }; + kbnTestServer.makeRequest(kbnServer, options, function (res) { + try { + expect(res.payload).to.match(/defaultRoute = '\/kibana\/app\/kibana'/); + done(); + } catch (e) { + done(e); + } + }); + }); +}); diff --git a/src/server/config/Config.js b/src/server/config/Config.js deleted file mode 100644 index b4291c63a8aea..0000000000000 --- a/src/server/config/Config.js +++ /dev/null @@ -1,167 +0,0 @@ -let Promise = require('bluebird'); -let Joi = require('joi'); -let _ = require('lodash'); -let { zipObject } = require('lodash'); -let override = require('./override'); -let pkg = require('requirefrom')('src/utils')('packageJson'); -const clone = require('./deepCloneWithBuffers'); - -const schema = Symbol('Joi Schema'); -const schemaKeys = Symbol('Schema Extensions'); -const vals = Symbol('config values'); -const pendingSets = Symbol('Pending Settings'); - -module.exports = class Config { - constructor(initialSchema, initialSettings) { - this[schemaKeys] = new Map(); - - this[vals] = Object.create(null); - this[pendingSets] = new Map(_.pairs(clone(initialSettings || {}))); - - if (initialSchema) this.extendSchema(initialSchema); - } - - getPendingSets() { - return this[pendingSets]; - } - - extendSchema(key, extension) { - if (key && key.isJoi) { - return _.each(key._inner.children, (child) => { - this.extendSchema(child.key, child.schema); - }); - } - - if (this.has(key)) { - throw new Error(`Config schema already has key: ${key}`); - } - - this[schemaKeys].set(key, extension); - this[schema] = null; - - let initialVals = this[pendingSets].get(key); - if (initialVals) { - this.set(key, initialVals); - this[pendingSets].delete(key); - } else { - this._commit(this[vals]); - } - } - - removeSchema(key) { - if (!this[schemaKeys].has(key)) { - throw new TypeError(`Unknown schema key: ${key}`); - } - - this[schema] = null; - this[schemaKeys].delete(key); - this[pendingSets].delete(key); - delete this[vals][key]; - } - - resetTo(obj) { - this._commit(obj); - } - - set(key, value) { - // clone and modify the config - let config = clone(this[vals]); - if (_.isPlainObject(key)) { - config = override(config, key); - } else { - _.set(config, key, value); - } - - // attempt to validate the config value - this._commit(config); - } - - _commit(newVals) { - // resolve the current environment - let env = newVals.env; - delete newVals.env; - if (_.isObject(env)) env = env.name; - if (!env) env = process.env.NODE_ENV || 'production'; - - let dev = env === 'development'; - let prod = env === 'production'; - - // pass the environment as context so that it can be refed in config - let context = { - env: env, - prod: prod, - dev: dev, - notProd: !prod, - notDev: !dev, - version: _.get(pkg, 'version'), - buildNum: dev ? Math.pow(2, 53) - 1 : _.get(pkg, 'build.number', NaN), - buildSha: dev ? 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' : _.get(pkg, 'build.sha', '') - }; - - if (!context.dev && !context.prod) { - throw new TypeError( - `Unexpected environment "${env}", expected one of "development" or "production"` - ); - } - - let results = Joi.validate(newVals, this.getSchema(), { context }); - - if (results.error) { - throw results.error; - } - - this[vals] = results.value; - } - - get(key) { - if (!key) { - return clone(this[vals]); - } - - let value = _.get(this[vals], key); - if (value === undefined) { - if (!this.has(key)) { - throw new Error('Unknown config key: ' + key); - } - } - return clone(value); - } - - has(key) { - function has(key, schema, path) { - path = path || []; - // Catch the partial paths - if (path.join('.') === key) return true; - // Only go deep on inner objects with children - if (schema._inner.children.length) { - for (let i = 0; i < schema._inner.children.length; i++) { - let child = schema._inner.children[i]; - // If the child is an object recurse through it's children and return - // true if there's a match - if (child.schema._type === 'object') { - if (has(key, child.schema, path.concat([child.key]))) return true; - // if the child matches, return true - } else if (path.concat([child.key]).join('.') === key) { - return true; - } - } - } - } - - if (_.isArray(key)) { - // TODO: add .has() support for array keys - key = key.join('.'); - } - - return !!has(key, this.getSchema()); - } - - getSchema() { - if (!this[schema]) { - let objKeys = zipObject([...this[schemaKeys]]); - this[schema] = Joi.object().keys(objKeys).default(); - } - - return this[schema]; - } -}; diff --git a/src/server/config/__tests__/config.js b/src/server/config/__tests__/config.js index 80ae95e33ce35..cf43ce256f585 100644 --- a/src/server/config/__tests__/config.js +++ b/src/server/config/__tests__/config.js @@ -1,7 +1,7 @@ -var Config = require('../Config'); -var expect = require('expect.js'); -var _ = require('lodash'); -var Joi = require('joi'); +import Config from '../config'; +import expect from 'expect.js'; +import _ from 'lodash'; +import Joi from 'joi'; /** * Plugins should defined a config method that takes a joi object. By default @@ -9,12 +9,12 @@ var Joi = require('joi'); * * Config should be newed up with a joi schema (containing defaults via joi) * - * var schema = { ... } + * let schema = { ... } * new Config(schema); * */ -var data = { +let data = { test: { hosts: ['host-01', 'host-02'], client: { @@ -25,7 +25,7 @@ var data = { } }; -var schema = Joi.object({ +let schema = Joi.object({ test: Joi.object({ enable: Joi.boolean().default(true), hosts: Joi.array().items(Joi.string()), @@ -43,17 +43,40 @@ describe('lib/config/config', function () { describe('constructor', function () { - it('should not allow any config if the schema is not passed', function (done) { - var config = new Config(); - var run = function () { + it('should not allow any config if the schema is not passed', function () { + let config = new Config(); + let run = function () { config.set('something.enable', true); }; expect(run).to.throwException(); - done(); + }); + + it('should allow keys in the schema', function () { + let config = new Config(schema); + let run = function () { + config.set('test.client.host', 'http://0.0.0.0'); + }; + expect(run).to.not.throwException(); + }); + + it('should not allow keys not in the schema', function () { + let config = new Config(schema); + let run = function () { + config.set('paramNotDefinedInTheSchema', true); + }; + expect(run).to.throwException(); + }); + + it('should not allow child keys not in the schema', function () { + let config = new Config(schema); + let run = function () { + config.set('test.client.paramNotDefinedInTheSchema', true); + }; + expect(run).to.throwException(); }); it('should set defaults', function () { - var config = new Config(schema); + let config = new Config(schema); expect(config.get('test.enable')).to.be(true); expect(config.get('test.client.type')).to.be('datastore'); }); @@ -62,14 +85,14 @@ describe('lib/config/config', function () { describe('#resetTo(object)', function () { - var config; + let config; beforeEach(function () { config = new Config(schema); }); it('should reset the config object with new values', function () { config.set(data); - var newData = config.get(); + let newData = config.get(); newData.test.enable = false; config.resetTo(newData); expect(config.get()).to.eql(newData); @@ -79,7 +102,7 @@ describe('lib/config/config', function () { describe('#has(key)', function () { - var config; + let config; beforeEach(function () { config = new Config(schema); }); @@ -99,7 +122,7 @@ describe('lib/config/config', function () { }); describe('#set(key, value)', function () { - var config; + let config; beforeEach(function () { config = new Config(schema); @@ -111,21 +134,21 @@ describe('lib/config/config', function () { }); it('should use an object to set config values', function () { - var hosts = ['host-01', 'host-02']; + let hosts = ['host-01', 'host-02']; config.set({ test: { enable: false, hosts: hosts } }); expect(config.get('test.enable')).to.be(false); expect(config.get('test.hosts')).to.eql(hosts); }); it('should use a flatten object to set config values', function () { - var hosts = ['host-01', 'host-02']; + let hosts = ['host-01', 'host-02']; config.set({ 'test.enable': false, 'test.hosts': hosts }); expect(config.get('test.enable')).to.be(false); expect(config.get('test.hosts')).to.eql(hosts); }); it('should override values with just the values present', function () { - var newData = _.cloneDeep(data); + let newData = _.cloneDeep(data); config.set(data); newData.test.enable = false; config.set({ test: { enable: false } }); @@ -133,7 +156,7 @@ describe('lib/config/config', function () { }); it('should thow an exception when setting a value with the wrong type', function (done) { - var run = function () { + let run = function () { config.set('test.enable', 'something'); }; expect(run).to.throwException(function (err) { @@ -148,7 +171,7 @@ describe('lib/config/config', function () { describe('#get(key)', function () { - var config; + let config; beforeEach(function () { config = new Config(schema); @@ -156,7 +179,7 @@ describe('lib/config/config', function () { }); it('should return the whole config object when called without a key', function () { - var newData = _.cloneDeep(data); + let newData = _.cloneDeep(data); newData.test.enable = true; expect(config.get()).to.eql(newData); }); @@ -171,14 +194,14 @@ describe('lib/config/config', function () { }); it('should throw exception for unknown config values', function () { - var run = function () { + let run = function () { config.get('test.does.not.exist'); }; expect(run).to.throwException(/Unknown config key: test.does.not.exist/); }); it('should not throw exception for undefined known config values', function () { - var run = function getUndefValue() { + let run = function getUndefValue() { config.get('test.undefValue'); }; expect(run).to.not.throwException(); @@ -187,20 +210,28 @@ describe('lib/config/config', function () { }); describe('#extendSchema(key, schema)', function () { - var config; + let config; beforeEach(function () { config = new Config(schema); }); it('should allow you to extend the schema at the top level', function () { - var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); + let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); config.extendSchema('myTest', newSchema); expect(config.get('myTest.test')).to.be(true); }); + it('should allow you to extend the schema with a prefix', function () { + let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); + config.extendSchema('prefix.myTest', newSchema); + expect(config.get('prefix')).to.eql({ myTest: { test: true }}); + expect(config.get('prefix.myTest')).to.eql({ test: true }); + expect(config.get('prefix.myTest.test')).to.be(true); + }); + it('should NOT allow you to extend the schema if somethign else is there', function () { - var newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); - var run = function () { + let newSchema = Joi.object({ test: Joi.boolean().default(true) }).default(); + let run = function () { config.extendSchema('test', newSchema); }; expect(run).to.throwException(); @@ -210,7 +241,7 @@ describe('lib/config/config', function () { describe('#removeSchema(key)', function () { it('should completely remove the key', function () { - var config = new Config(Joi.object().keys({ + let config = new Config(Joi.object().keys({ a: Joi.number().default(1) })); @@ -220,7 +251,7 @@ describe('lib/config/config', function () { }); it('only removes existing keys', function () { - var config = new Config(Joi.object()); + let config = new Config(Joi.object()); expect(() => config.removeSchema('b')).to.throwException('Unknown schema'); }); @@ -228,4 +259,3 @@ describe('lib/config/config', function () { }); }); - diff --git a/src/server/config/__tests__/deepCloneWithBuffers.js b/src/server/config/__tests__/deepCloneWithBuffers.js deleted file mode 100644 index 6482c5f24ed5d..0000000000000 --- a/src/server/config/__tests__/deepCloneWithBuffers.js +++ /dev/null @@ -1,61 +0,0 @@ -import deepCloneWithBuffers from '../deepCloneWithBuffers'; -import expect from 'expect.js'; - -describe('deepCloneWithBuffers()', function () { - it('deep clones objects', function () { - const source = { - a: { - b: {}, - c: {}, - d: [ - { - e: 'f' - } - ] - } - }; - - const output = deepCloneWithBuffers(source); - - expect(source.a).to.eql(output.a); - expect(source.a).to.not.be(output.a); - - expect(source.a.b).to.eql(output.a.b); - expect(source.a.b).to.not.be(output.a.b); - - expect(source.a.c).to.eql(output.a.c); - expect(source.a.c).to.not.be(output.a.c); - - expect(source.a.d).to.eql(output.a.d); - expect(source.a.d).to.not.be(output.a.d); - - expect(source.a.d[0]).to.eql(output.a.d[0]); - expect(source.a.d[0]).to.not.be(output.a.d[0]); - }); - - it('copies buffers but keeps them buffers', function () { - const input = new Buffer('i am a teapot', 'utf8'); - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input)).to.be.ok(); - expect(Buffer.isBuffer(output)).to.be.ok(); - expect(Buffer.compare(output, input)); - expect(output).to.not.be(input); - }); - - it('copies buffers that are deep', function () { - const input = { - a: { - b: { - c: new Buffer('i am a teapot', 'utf8') - } - } - }; - const output = deepCloneWithBuffers(input); - - expect(Buffer.isBuffer(input.a.b.c)).to.be.ok(); - expect(Buffer.isBuffer(output.a.b.c)).to.be.ok(); - expect(Buffer.compare(output.a.b.c, input.a.b.c)); - expect(output.a.b.c).to.not.be(input.a.b.c); - }); -}); diff --git a/src/server/config/__tests__/deep_clone_with_buffers.js b/src/server/config/__tests__/deep_clone_with_buffers.js new file mode 100644 index 0000000000000..02bb17c5c430c --- /dev/null +++ b/src/server/config/__tests__/deep_clone_with_buffers.js @@ -0,0 +1,61 @@ +import deepCloneWithBuffers from '../deep_clone_with_buffers'; +import expect from 'expect.js'; + +describe('deepCloneWithBuffers()', function () { + it('deep clones objects', function () { + const source = { + a: { + b: {}, + c: {}, + d: [ + { + e: 'f' + } + ] + } + }; + + const output = deepCloneWithBuffers(source); + + expect(source.a).to.eql(output.a); + expect(source.a).to.not.be(output.a); + + expect(source.a.b).to.eql(output.a.b); + expect(source.a.b).to.not.be(output.a.b); + + expect(source.a.c).to.eql(output.a.c); + expect(source.a.c).to.not.be(output.a.c); + + expect(source.a.d).to.eql(output.a.d); + expect(source.a.d).to.not.be(output.a.d); + + expect(source.a.d[0]).to.eql(output.a.d[0]); + expect(source.a.d[0]).to.not.be(output.a.d[0]); + }); + + it('copies buffers but keeps them buffers', function () { + const input = new Buffer('i am a teapot', 'utf8'); + const output = deepCloneWithBuffers(input); + + expect(Buffer.isBuffer(input)).to.be.ok(); + expect(Buffer.isBuffer(output)).to.be.ok(); + expect(Buffer.compare(output, input)); + expect(output).to.not.be(input); + }); + + it('copies buffers that are deep', function () { + const input = { + a: { + b: { + c: new Buffer('i am a teapot', 'utf8') + } + } + }; + const output = deepCloneWithBuffers(input); + + expect(Buffer.isBuffer(input.a.b.c)).to.be.ok(); + expect(Buffer.isBuffer(output.a.b.c)).to.be.ok(); + expect(Buffer.compare(output.a.b.c, input.a.b.c)); + expect(output.a.b.c).to.not.be(input.a.b.c); + }); +}); diff --git a/src/server/config/__tests__/explodeBy.js b/src/server/config/__tests__/explodeBy.js deleted file mode 100644 index cfda593e9d59f..0000000000000 --- a/src/server/config/__tests__/explodeBy.js +++ /dev/null @@ -1,32 +0,0 @@ -var explodeBy = require('../explodeBy'); -var expect = require('expect.js'); - -describe('explode_by(dot, flatObject)', function () { - - it('should explode a flatten object with dots', function () { - var flatObject = { - 'test.enable': true, - 'test.hosts': ['host-01', 'host-02'] - }; - expect(explodeBy('.', flatObject)).to.eql({ - test: { - enable: true, - hosts: ['host-01', 'host-02'] - } - }); - }); - - it('should explode a flatten object with slashes', function () { - var flatObject = { - 'test/enable': true, - 'test/hosts': ['host-01', 'host-02'] - }; - expect(explodeBy('/', flatObject)).to.eql({ - test: { - enable: true, - hosts: ['host-01', 'host-02'] - } - }); - }); - -}); diff --git a/src/server/config/__tests__/explode_by.js b/src/server/config/__tests__/explode_by.js new file mode 100644 index 0000000000000..3514413e575b6 --- /dev/null +++ b/src/server/config/__tests__/explode_by.js @@ -0,0 +1,32 @@ +import explodeBy from '../explode_by'; +import expect from 'expect.js'; + +describe('explode_by(dot, flatObject)', function () { + + it('should explode a flatten object with dots', function () { + let flatObject = { + 'test.enable': true, + 'test.hosts': ['host-01', 'host-02'] + }; + expect(explodeBy('.', flatObject)).to.eql({ + test: { + enable: true, + hosts: ['host-01', 'host-02'] + } + }); + }); + + it('should explode a flatten object with slashes', function () { + let flatObject = { + 'test/enable': true, + 'test/hosts': ['host-01', 'host-02'] + }; + expect(explodeBy('/', flatObject)).to.eql({ + test: { + enable: true, + hosts: ['host-01', 'host-02'] + } + }); + }); + +}); diff --git a/src/server/config/__tests__/flattenWith.js b/src/server/config/__tests__/flattenWith.js deleted file mode 100644 index 3795f9ba41d0f..0000000000000 --- a/src/server/config/__tests__/flattenWith.js +++ /dev/null @@ -1,27 +0,0 @@ -var flattenWith = require('../flattenWith'); -var expect = require('expect.js'); - -describe('flatten_with(dot, nestedObj)', function () { - - it('should flatten object with dot', function () { - var nestedObj = { - test: { - enable: true, - hosts: ['host-01', 'host-02'], - client: { - type: 'nosql', - pool: [{ port: 5051 }, { port: 5052 }] - } - } - }; - expect(flattenWith('.', nestedObj)).to.eql({ - 'test.enable': true, - 'test.hosts': ['host-01', 'host-02'], - 'test.client.type': 'nosql', - 'test.client.pool': [{ port: 5051 }, { port: 5052 }] - }); - }); - -}); - - diff --git a/src/server/config/__tests__/flatten_with.js b/src/server/config/__tests__/flatten_with.js new file mode 100644 index 0000000000000..9dc40fc08097e --- /dev/null +++ b/src/server/config/__tests__/flatten_with.js @@ -0,0 +1,27 @@ +import flattenWith from '../flatten_with'; +import expect from 'expect.js'; + +describe('flatten_with(dot, nestedObj)', function () { + + it('should flatten object with dot', function () { + let nestedObj = { + test: { + enable: true, + hosts: ['host-01', 'host-02'], + client: { + type: 'nosql', + pool: [{ port: 5051 }, { port: 5052 }] + } + } + }; + expect(flattenWith('.', nestedObj)).to.eql({ + 'test.enable': true, + 'test.hosts': ['host-01', 'host-02'], + 'test.client.type': 'nosql', + 'test.client.pool': [{ port: 5051 }, { port: 5052 }] + }); + }); + +}); + + diff --git a/src/server/config/__tests__/override.js b/src/server/config/__tests__/override.js index cac9e4340cd06..18b3117dcbe56 100644 --- a/src/server/config/__tests__/override.js +++ b/src/server/config/__tests__/override.js @@ -1,10 +1,10 @@ -var override = require('../override'); -var expect = require('expect.js'); +import override from '../override'; +import expect from 'expect.js'; describe('override(target, source)', function () { it('should override the values form source to target', function () { - var target = { + let target = { test: { enable: true, host: ['host-01', 'host-02'], @@ -13,7 +13,7 @@ describe('override(target, source)', function () { } } }; - var source = { test: { client: { type: 'nosql' } } }; + let source = { test: { client: { type: 'nosql' } } }; expect(override(target, source)).to.eql({ test: { enable: true, diff --git a/src/server/config/__tests__/unset.js b/src/server/config/__tests__/unset.js new file mode 100644 index 0000000000000..5b26fccdf68c2 --- /dev/null +++ b/src/server/config/__tests__/unset.js @@ -0,0 +1,83 @@ +import unset from '../unset'; +import expect from 'expect.js'; + +describe('unset(obj, key)', function () { + describe('invalid input', function () { + it('should do nothing if not given an object', function () { + const obj = 'hello'; + unset(obj, 'e'); + expect(obj).to.equal('hello'); + }); + + it('should do nothing if not given a key', function () { + const obj = { one: 1 }; + unset(obj); + expect(obj).to.eql({ one: 1 }); + }); + + it('should do nothing if given an empty string as a key', function () { + const obj = { one: 1 }; + unset(obj, ''); + expect(obj).to.eql({ one: 1 }); + }); + }); + + describe('shallow removal', function () { + let obj; + + beforeEach(function () { + obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; + }); + + it('should remove the param using a string key', function () { + unset(obj, 'two'); + expect(obj).to.eql({ one: 1, deep: { three: 3, four: 4 } }); + }); + + it('should remove the param using an array key', function () { + unset(obj, ['two']); + expect(obj).to.eql({ one: 1, deep: { three: 3, four: 4 } }); + }); + }); + + describe('deep removal', function () { + let obj; + + beforeEach(function () { + obj = { one: 1, two: 2, deep: { three: 3, four: 4 } }; + }); + + it('should remove the param using a string key', function () { + unset(obj, 'deep.three'); + expect(obj).to.eql({ one: 1, two: 2, deep: { four: 4 } }); + }); + + it('should remove the param using an array key', function () { + unset(obj, ['deep', 'three']); + expect(obj).to.eql({ one: 1, two: 2, deep: { four: 4 } }); + }); + }); + + describe('recursive removal', function () { + it('should clear object if only value is removed', function () { + const obj = { one: { two: { three: 3 } } }; + unset(obj, 'one.two.three'); + expect(obj).to.eql({}); + }); + + it('should clear object if no props are left', function () { + const obj = { one: { two: { three: 3 } } }; + unset(obj, 'one.two'); + expect(obj).to.eql({}); + }); + + it('should remove deep property, then clear the object', function () { + const obj = { one: { two: { three: 3, four: 4 } } }; + unset(obj, 'one.two.three'); + expect(obj).to.eql({ one: { two: { four: 4 } } }); + + unset(obj, 'one.two.four'); + expect(obj).to.eql({}); + }); + }); +}); diff --git a/src/server/config/complete.js b/src/server/config/complete.js index f4c55170bfa2e..be44448733782 100644 --- a/src/server/config/complete.js +++ b/src/server/config/complete.js @@ -1,4 +1,4 @@ -module.exports = function (kbnServer, server, config) { +export default function (kbnServer, server, config) { server.decorate('server', 'config', function () { return kbnServer.config; diff --git a/src/server/config/config.js b/src/server/config/config.js new file mode 100644 index 0000000000000..0c0f3b75c1531 --- /dev/null +++ b/src/server/config/config.js @@ -0,0 +1,185 @@ +import Promise from 'bluebird'; +import Joi from 'joi'; +import _ from 'lodash'; +import override from './override'; +import unset from './unset'; +import createDefaultSchema from './schema'; +import pkg from '../../utils/package_json'; +import clone from './deep_clone_with_buffers'; + +const schema = Symbol('Joi Schema'); +const schemaExts = Symbol('Schema Extensions'); +const vals = Symbol('config values'); +const pendingSets = Symbol('Pending Settings'); + +module.exports = class Config { + static withDefaultSchema(settings = {}) { + return new Config(createDefaultSchema(), settings); + } + + constructor(initialSchema, initialSettings) { + this[schemaExts] = Object.create(null); + this[vals] = Object.create(null); + this[pendingSets] = _.merge(Object.create(null), initialSettings || {}); + + if (initialSchema) this.extendSchema(initialSchema); + } + + getPendingSets() { + return new Map(_.pairs(this[pendingSets])); + } + + extendSchema(key, extension) { + if (key && key.isJoi) { + return _.each(key._inner.children, (child) => { + this.extendSchema(child.key, child.schema); + }); + } + + if (this.has(key)) { + throw new Error(`Config schema already has key: ${key}`); + } + + _.set(this[schemaExts], key, extension); + this[schema] = null; + + let initialVals = _.get(this[pendingSets], key); + if (initialVals) { + this.set(key, initialVals); + unset(this[pendingSets], key); + } else { + this._commit(this[vals]); + } + } + + removeSchema(key) { + if (!_.has(this[schemaExts], key)) { + throw new TypeError(`Unknown schema key: ${key}`); + } + + this[schema] = null; + unset(this[schemaExts], key); + unset(this[pendingSets], key); + unset(this[vals], key); + } + + resetTo(obj) { + this._commit(obj); + } + + set(key, value) { + // clone and modify the config + let config = clone(this[vals]); + if (_.isPlainObject(key)) { + config = override(config, key); + } else { + _.set(config, key, value); + } + + // attempt to validate the config value + this._commit(config); + } + + _commit(newVals) { + // resolve the current environment + let env = newVals.env; + delete newVals.env; + if (_.isObject(env)) env = env.name; + if (!env) env = process.env.NODE_ENV || 'production'; + + let dev = env === 'development'; + let prod = env === 'production'; + + // pass the environment as context so that it can be refed in config + let context = { + env: env, + prod: prod, + dev: dev, + notProd: !prod, + notDev: !dev, + version: _.get(pkg, 'version'), + buildNum: dev ? Math.pow(2, 53) - 1 : _.get(pkg, 'build.number', NaN), + buildSha: dev ? 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' : _.get(pkg, 'build.sha', '') + }; + + if (!context.dev && !context.prod) { + throw new TypeError( + `Unexpected environment "${env}", expected one of "development" or "production"` + ); + } + + let results = Joi.validate(newVals, this.getSchema(), { context }); + + if (results.error) { + throw results.error; + } + + this[vals] = results.value; + } + + get(key) { + if (!key) { + return clone(this[vals]); + } + + let value = _.get(this[vals], key); + if (value === undefined) { + if (!this.has(key)) { + throw new Error('Unknown config key: ' + key); + } + } + return clone(value); + } + + has(key) { + function has(key, schema, path) { + path = path || []; + // Catch the partial paths + if (path.join('.') === key) return true; + // Only go deep on inner objects with children + if (_.size(schema._inner.children)) { + for (let i = 0; i < schema._inner.children.length; i++) { + let child = schema._inner.children[i]; + // If the child is an object recurse through it's children and return + // true if there's a match + if (child.schema._type === 'object') { + if (has(key, child.schema, path.concat([child.key]))) return true; + // if the child matches, return true + } else if (path.concat([child.key]).join('.') === key) { + return true; + } + } + } + } + + if (_.isArray(key)) { + // TODO: add .has() support for array keys + key = key.join('.'); + } + + return !!has(key, this.getSchema()); + } + + getSchema() { + if (!this[schema]) { + this[schema] = (function convertToSchema(children) { + let schema = Joi.object().keys({}).default(); + + for (const key of Object.keys(children)) { + const child = children[key]; + const childSchema = _.isPlainObject(child) ? convertToSchema(child) : child; + + if (!childSchema || !childSchema.isJoi) { + throw new TypeError('Unable to convert configuration definition value to Joi schema: ' + childSchema); + } + + schema = schema.keys({ [key]: childSchema }); + } + + return schema; + }(this[schemaExts])); + } + + return this[schema]; + } +}; diff --git a/src/server/config/deepCloneWithBuffers.js b/src/server/config/deep_clone_with_buffers.js similarity index 100% rename from src/server/config/deepCloneWithBuffers.js rename to src/server/config/deep_clone_with_buffers.js diff --git a/src/server/config/explodeBy.js b/src/server/config/explodeBy.js deleted file mode 100644 index ca30b346ea82a..0000000000000 --- a/src/server/config/explodeBy.js +++ /dev/null @@ -1,19 +0,0 @@ -var _ = require('lodash'); -module.exports = function (dot, flatObject) { - var fullObject = {}; - _.each(flatObject, function (value, key) { - var keys = key.split(dot); - (function walk(memo, keys, value) { - var _key = keys.shift(); - if (keys.length === 0) { - memo[_key] = value; - } else { - if (!memo[_key]) memo[_key] = {}; - walk(memo[_key], keys, value); - } - }(fullObject, keys, value)); - }); - return fullObject; -}; - - diff --git a/src/server/config/explode_by.js b/src/server/config/explode_by.js new file mode 100644 index 0000000000000..50e2b2e82615f --- /dev/null +++ b/src/server/config/explode_by.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +module.exports = function (dot, flatObject) { + let fullObject = {}; + _.each(flatObject, function (value, key) { + let keys = key.split(dot); + (function walk(memo, keys, value) { + let _key = keys.shift(); + if (keys.length === 0) { + memo[_key] = value; + } else { + if (!memo[_key]) memo[_key] = {}; + walk(memo[_key], keys, value); + } + }(fullObject, keys, value)); + }); + return fullObject; +}; + + diff --git a/src/server/config/flattenWith.js b/src/server/config/flattenWith.js deleted file mode 100644 index 8718c9c4313c0..0000000000000 --- a/src/server/config/flattenWith.js +++ /dev/null @@ -1,18 +0,0 @@ -var _ = require('lodash'); -module.exports = function (dot, nestedObj, flattenArrays) { - var key; // original key - var stack = []; // track key stack - var flatObj = {}; - (function flattenObj(obj) { - _.keys(obj).forEach(function (key) { - stack.push(key); - if (!flattenArrays && _.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key]; - else if (_.isObject(obj[key])) flattenObj(obj[key]); - else flatObj[stack.join(dot)] = obj[key]; - stack.pop(); - }); - }(nestedObj)); - return flatObj; -}; - - diff --git a/src/server/config/flatten_with.js b/src/server/config/flatten_with.js new file mode 100644 index 0000000000000..d4a276607bfc7 --- /dev/null +++ b/src/server/config/flatten_with.js @@ -0,0 +1,18 @@ +import _ from 'lodash'; +module.exports = function (dot, nestedObj, flattenArrays) { + let key; // original key + let stack = []; // track key stack + let flatObj = {}; + (function flattenObj(obj) { + _.keys(obj).forEach(function (key) { + stack.push(key); + if (!flattenArrays && _.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key]; + else if (_.isObject(obj[key])) flattenObj(obj[key]); + else flatObj[stack.join(dot)] = obj[key]; + stack.pop(); + }); + }(nestedObj)); + return flatObj; +}; + + diff --git a/src/server/config/override.js b/src/server/config/override.js index a14ccb980706c..ef850a32754ae 100644 --- a/src/server/config/override.js +++ b/src/server/config/override.js @@ -1,10 +1,10 @@ -var _ = require('lodash'); -var flattenWith = require('./flattenWith'); -var explodeBy = require('./explodeBy'); +import _ from 'lodash'; +import flattenWith from './flatten_with'; +import explodeBy from './explode_by'; module.exports = function (target, source) { - var _target = flattenWith('.', target); - var _source = flattenWith('.', source); + let _target = flattenWith('.', target); + let _source = flattenWith('.', source); return explodeBy('.', _.defaults(_source, _target)); }; diff --git a/src/server/config/schema.js b/src/server/config/schema.js index 763ee22db591d..55f4d31bad6c3 100644 --- a/src/server/config/schema.js +++ b/src/server/config/schema.js @@ -1,11 +1,11 @@ -let get = require('lodash').get; -let Joi = require('joi'); -let fs = require('fs'); -let path = require('path'); +import Joi from 'joi'; +import fs from 'fs'; +import path from 'path'; +import { get } from 'lodash'; +import { randomBytes } from 'crypto'; +import os from 'os'; -let utils = require('requirefrom')('src/utils'); -let fromRoot = utils('fromRoot'); -const randomBytes = require('crypto').randomBytes; +import { fromRoot } from '../../utils'; module.exports = () => Joi.object({ pkg: Joi.object({ @@ -20,16 +20,24 @@ module.exports = () => Joi.object({ prod: Joi.boolean().default(Joi.ref('$prod')) }).default(), + dev: Joi.object({ + basePathProxyTarget: Joi.number().default(5603), + }).default(), + pid: Joi.object({ file: Joi.string(), exclusive: Joi.boolean().default(false) }).default(), + uuid: Joi.string().guid().default(), + server: Joi.object({ + name: Joi.string().default(os.hostname()), host: Joi.string().hostname().default('0.0.0.0'), port: Joi.number().default(5601), + maxPayloadBytes: Joi.number().default(1048576), autoListen: Joi.boolean().default(true), - defaultRoute: Joi.string(), + defaultRoute: Joi.string().default('/app/kibana').regex(/^\//, `start with a slash`), basePath: Joi.string().default('').allow('').regex(/(^$|^\/.*[^\/]$)/, `start with a slash, don't end with one`), ssl: Joi.object({ cert: Joi.string(), @@ -43,8 +51,8 @@ module.exports = () => Joi.object({ otherwise: Joi.boolean().default(false) }), xsrf: Joi.object({ - token: Joi.string().default(randomBytes(32).toString('hex')), disableProtection: Joi.boolean().default(false), + token: Joi.string().optional().notes('Deprecated') }).default(), }).default(), @@ -77,6 +85,10 @@ module.exports = () => Joi.object({ }) .default(), + ops: Joi.object({ + interval: Joi.number().default(5000), + }), + plugins: Joi.object({ paths: Joi.array().items(Joi.string()).default([]), scanDirs: Joi.array().items(Joi.string()).default([]), @@ -109,6 +121,10 @@ module.exports = () => Joi.object({ ) .default(Joi.ref('$dev')), profile: Joi.boolean().default(false) + }).default(), + + status: Joi.object({ + allowAnonymous: Joi.boolean().default(false) }).default() }).default(); diff --git a/src/server/config/setup.js b/src/server/config/setup.js index e1728111a2de0..dcecb0c2abb79 100644 --- a/src/server/config/setup.js +++ b/src/server/config/setup.js @@ -1,6 +1,4 @@ +import Config from './config'; module.exports = function (kbnServer) { - let Config = require('./Config'); - let schema = require('./schema')(); - - kbnServer.config = new Config(schema, kbnServer.settings || {}); + kbnServer.config = Config.withDefaultSchema(kbnServer.settings); }; diff --git a/src/server/config/unset.js b/src/server/config/unset.js new file mode 100644 index 0000000000000..60de86dcb720d --- /dev/null +++ b/src/server/config/unset.js @@ -0,0 +1,26 @@ +import _ from 'lodash'; +import toPath from 'lodash/internal/toPath'; + +module.exports = function unset(object, rawPath) { + if (!object) return; + const path = toPath(rawPath); + + switch (path.length) { + case 0: + return; + + case 1: + delete object[rawPath]; + break; + + default: + const leaf = path.pop(); + const parentPath = path.slice(); + const parent = _.get(object, parentPath); + unset(parent, leaf); + if (!_.size(parent)) { + unset(object, parentPath); + } + break; + } +}; \ No newline at end of file diff --git a/src/server/http/__tests__/index.js b/src/server/http/__tests__/index.js new file mode 100644 index 0000000000000..f9f72f89e2bcf --- /dev/null +++ b/src/server/http/__tests__/index.js @@ -0,0 +1,87 @@ +import expect from 'expect.js'; +import * as kbnTestServer from '../../../../test/utils/kbn_server'; +import fromRoot from '../../../utils/from_root'; + +describe('routes', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + beforeEach(function () { + kbnServer = kbnTestServer.createServer({ + plugins: { + scanDirs: [ + fromRoot('src/plugins') + ] + } + }); + return kbnServer.ready(); + }); + afterEach(function () { + return kbnServer.close(); + }); + + describe('cookie validation', function () { + it('allows non-strict cookies', function (done) { + const options = { + method: 'GET', + url: '/', + headers: { + cookie: 'test:80=value;test_80=value' + } + }; + kbnTestServer.makeRequest(kbnServer, options, (res) => { + expect(res.payload).not.to.contain('Invalid cookie header'); + done(); + }); + }); + + it('returns an error if the cookie can\'t be parsed', function (done) { + const options = { + method: 'GET', + url: '/', + headers: { + cookie: 'a' + } + }; + kbnTestServer.makeRequest(kbnServer, options, (res) => { + expect(res.payload).to.contain('Invalid cookie header'); + done(); + }); + }); + }); + + describe('url shortener', () => { + const shortenOptions = { + method: 'POST', + url: '/shorten', + payload: { + url: '/app/kibana#/visualize/create' + } + }; + + it('generates shortened urls', (done) => { + kbnTestServer.makeRequest(kbnServer, shortenOptions, (res) => { + expect(typeof res.payload).to.be('string'); + expect(res.payload.length > 0).to.be(true); + done(); + }); + }); + + it('redirects shortened urls', (done) => { + kbnTestServer.makeRequest(kbnServer, shortenOptions, (res) => { + const gotoOptions = { + method: 'GET', + url: '/goto/' + res.payload + }; + kbnTestServer.makeRequest(kbnServer, gotoOptions, (res) => { + expect(res.statusCode).to.be(302); + expect(res.headers.location).to.be(shortenOptions.payload.url); + done(); + }); + }); + }); + + }); + +}); diff --git a/src/server/http/__tests__/xsrf.js b/src/server/http/__tests__/xsrf.js index a38ca767edce4..61924aa62a726 100644 --- a/src/server/http/__tests__/xsrf.js +++ b/src/server/http/__tests__/xsrf.js @@ -1,28 +1,29 @@ import expect from 'expect.js'; import { fromNode as fn } from 'bluebird'; import { resolve } from 'path'; - -import KbnServer from '../../KbnServer'; +import * as kbnTestServer from '../../../../test/utils/kbn_server'; const nonDestructiveMethods = ['GET']; const destructiveMethods = ['POST', 'PUT', 'DELETE']; const src = resolve.bind(null, __dirname, '../../../../src'); +const xsrfHeader = 'kbn-version'; +const version = require(src('../package.json')).version; + describe('xsrf request filter', function () { function inject(kbnServer, opts) { return fn(cb => { - kbnServer.server.inject(opts, (resp) => { + kbnTestServer.makeRequest(kbnServer, opts, (resp) => { cb(null, resp); }); }); } - const makeServer = async function (token) { - const kbnServer = new KbnServer({ - server: { autoListen: false, xsrf: { token } }, - plugins: { scanDirs: [src('plugins')] }, - logging: { quiet: true }, - optimize: { enabled: false }, + const makeServer = async function () { + const kbnServer = kbnTestServer.createServer({ + server: { + xsrf: { disableProtection: false } + } }); await kbnServer.ready(); @@ -38,108 +39,75 @@ describe('xsrf request filter', function () { return kbnServer; }; - describe('issuing tokens', function () { - const token = 'secur3'; - let kbnServer; - beforeEach(async () => kbnServer = await makeServer(token)); - afterEach(async () => await kbnServer.close()); + let kbnServer; + beforeEach(async () => kbnServer = await makeServer()); + afterEach(async () => await kbnServer.close()); - it('sends a token when rendering an app', async function () { - var resp = await inject(kbnServer, { - method: 'GET', - url: '/app/kibana', - }); + for (const method of nonDestructiveMethods) { + context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func + it('accepts requests without a token', async function () { + const resp = await inject(kbnServer, { + url: '/xsrf/test/route', + method: method + }); - expect(resp.payload).to.contain(`"xsrfToken":"${token}"`); - }); - }); + expect(resp.statusCode).to.be(200); + expect(resp.payload).to.be('ok'); + }); - context('without configured token', function () { - let kbnServer; - beforeEach(async () => kbnServer = await makeServer()); - afterEach(async () => await kbnServer.close()); + it('failes on invalid tokens', async function () { + const resp = await inject(kbnServer, { + url: '/xsrf/test/route', + method: method, + headers: { + [xsrfHeader]: `invalid:${version}`, + }, + }); - it('responds with a random token', async function () { - var resp = await inject(kbnServer, { - method: 'GET', - url: '/app/kibana', + expect(resp.statusCode).to.be(400); + expect(resp.headers).to.have.property(xsrfHeader, version); + expect(resp.payload).to.match(/"Browser client is out of date/); }); - - expect(resp.payload).to.match(/"xsrfToken":".{64}"/); }); - }); - - context('with configured token', function () { - const token = 'mytoken'; - let kbnServer; - beforeEach(async () => kbnServer = await makeServer(token)); - afterEach(async () => await kbnServer.close()); - - for (const method of nonDestructiveMethods) { - context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func - it('accepts requests without a token', async function () { - const resp = await inject(kbnServer, { - url: '/xsrf/test/route', - method: method - }); - - expect(resp.statusCode).to.be(200); - expect(resp.payload).to.be('ok'); - }); + } - it('ignores invalid tokens', async function () { - const resp = await inject(kbnServer, { - url: '/xsrf/test/route', - method: method, - headers: { - 'kbn-xsrf-token': `invalid:${token}`, - }, - }); - - expect(resp.statusCode).to.be(200); - expect(resp.headers).to.not.have.property('kbn-xsrf-token'); + for (const method of destructiveMethods) { + context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func + it('accepts requests with the correct token', async function () { + const resp = await inject(kbnServer, { + url: '/xsrf/test/route', + method: method, + headers: { + [xsrfHeader]: version, + }, }); + + expect(resp.statusCode).to.be(200); + expect(resp.payload).to.be('ok'); }); - } - - for (const method of destructiveMethods) { - context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func - it('accepts requests with the correct token', async function () { - const resp = await inject(kbnServer, { - url: '/xsrf/test/route', - method: method, - headers: { - 'kbn-xsrf-token': token, - }, - }); - - expect(resp.statusCode).to.be(200); - expect(resp.payload).to.be('ok'); + + it('rejects requests without a token', async function () { + const resp = await inject(kbnServer, { + url: '/xsrf/test/route', + method: method }); - it('rejects requests without a token', async function () { - const resp = await inject(kbnServer, { - url: '/xsrf/test/route', - method: method - }); + expect(resp.statusCode).to.be(400); + expect(resp.payload).to.match(/"Missing kbn-version header/); + }); - expect(resp.statusCode).to.be(403); - expect(resp.payload).to.match(/"Missing XSRF token"/); + it('rejects requests with an invalid token', async function () { + const resp = await inject(kbnServer, { + url: '/xsrf/test/route', + method: method, + headers: { + [xsrfHeader]: `invalid:${version}`, + }, }); - it('rejects requests with an invalid token', async function () { - const resp = await inject(kbnServer, { - url: '/xsrf/test/route', - method: method, - headers: { - 'kbn-xsrf-token': `invalid:${token}`, - }, - }); - - expect(resp.statusCode).to.be(403); - expect(resp.payload).to.match(/"Invalid XSRF token"/); - }); + expect(resp.statusCode).to.be(400); + expect(resp.payload).to.match(/"Browser client is out of date/); }); - } - }); + }); + } }); diff --git a/src/server/http/getDefaultRoute.js b/src/server/http/getDefaultRoute.js deleted file mode 100644 index 964787cfd4f3a..0000000000000 --- a/src/server/http/getDefaultRoute.js +++ /dev/null @@ -1,10 +0,0 @@ -let _ = require('lodash'); - -module.exports = _.once(function (kbnServer) { - const { uiExports, config } = kbnServer; - // user configured default route - let defaultConfig = config.get('server.defaultRoute'); - if (defaultConfig) return defaultConfig; - - return `${config.get('server.basePath')}/app/kibana`; -}); diff --git a/src/server/http/get_default_route.js b/src/server/http/get_default_route.js new file mode 100644 index 0000000000000..09d525bbb006a --- /dev/null +++ b/src/server/http/get_default_route.js @@ -0,0 +1,6 @@ +import _ from 'lodash'; + +module.exports = _.once(function (kbnServer) { + const { uiExports, config } = kbnServer; + return `${config.get('server.basePath')}${config.get('server.defaultRoute')}`; +}); diff --git a/src/server/http/index.js b/src/server/http/index.js index 26ecf8ed41767..b6440bec66044 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -1,33 +1,17 @@ -module.exports = function (kbnServer, server, config) { - let _ = require('lodash'); - let fs = require('fs'); - let Boom = require('boom'); - let Hapi = require('hapi'); - let parse = require('url').parse; - let format = require('url').format; +import { parse } from 'url'; +import { format } from 'url'; +import _ from 'lodash'; +import fs from 'fs'; +import Boom from 'boom'; +import Hapi from 'hapi'; +import getDefaultRoute from './get_default_route'; +module.exports = async function (kbnServer, server, config) { - let getDefaultRoute = require('./getDefaultRoute'); server = kbnServer.server = new Hapi.Server(); - // Create a new connection - var connectionOptions = { - host: config.get('server.host'), - port: config.get('server.port'), - routes: { - cors: config.get('server.cors') - } - }; - - // enable tls if ssl key and cert are defined - if (config.get('server.ssl.key') && config.get('server.ssl.cert')) { - connectionOptions.tls = { - key: fs.readFileSync(config.get('server.ssl.key')), - cert: fs.readFileSync(config.get('server.ssl.cert')) - }; - } - - server.connection(connectionOptions); + const shortUrlLookup = require('./short_url_lookup')(server); + await kbnServer.mixin(require('./setup_connection')); // provide a simple way to expose static directories server.decorate('server', 'exposeStaticDir', function (routePath, dirPath) { @@ -37,7 +21,7 @@ module.exports = function (kbnServer, server, config) { handler: { directory: { path: dirPath, - listing: true, + listing: false, lookupCompressed: true } }, @@ -84,11 +68,11 @@ module.exports = function (kbnServer, server, config) { let response = req.response; if (response.isBoom) { - response.output.headers['x-app-name'] = kbnServer.name; - response.output.headers['x-app-version'] = kbnServer.version; + response.output.headers['kbn-name'] = kbnServer.name; + response.output.headers['kbn-version'] = kbnServer.version; } else { - response.header('x-app-name', kbnServer.name); - response.header('x-app-version', kbnServer.version); + response.header('kbn-name', kbnServer.name); + response.header('kbn-version', kbnServer.version); } return reply.continue(); @@ -98,7 +82,7 @@ module.exports = function (kbnServer, server, config) { path: '/', method: 'GET', handler: function (req, reply) { - return reply.view('rootRedirect', { + return reply.view('root_redirect', { hashRoute: `${config.get('server.basePath')}/app/kibana`, defaultRoute: getDefaultRoute(kbnServer), }); @@ -122,5 +106,31 @@ module.exports = function (kbnServer, server, config) { } }); + server.route({ + method: 'GET', + path: '/goto/{urlId}', + handler: async function (request, reply) { + try { + const url = await shortUrlLookup.getUrl(request.params.urlId); + reply().redirect(config.get('server.basePath') + url); + } catch (err) { + reply(err); + } + } + }); + + server.route({ + method: 'POST', + path: '/shorten', + handler: async function (request, reply) { + try { + const urlId = await shortUrlLookup.generateUrlId(request.payload.url); + reply(urlId); + } catch (err) { + reply(err); + } + } + }); + return kbnServer.mixin(require('./xsrf')); }; diff --git a/src/server/http/setup_connection.js b/src/server/http/setup_connection.js new file mode 100644 index 0000000000000..cd922c7ca9366 --- /dev/null +++ b/src/server/http/setup_connection.js @@ -0,0 +1,63 @@ +import { readFileSync } from 'fs'; +import { format as formatUrl } from 'url'; +import httpolyglot from 'httpolyglot'; + +import tlsCiphers from './tls_ciphers'; + +export default function (kbnServer, server, config) { + // this mixin is used outside of the kbn server, so it MUST work without a full kbnServer object. + kbnServer = null; + + const host = config.get('server.host'); + const port = config.get('server.port'); + + const connectionOptions = { + host, + port, + state: { + strictHeader: false + }, + routes: { + cors: config.get('server.cors'), + payload: { + maxBytes: config.get('server.maxPayloadBytes') + } + } + }; + + // enable tlsOpts if ssl key and cert are defined + const useSsl = config.get('server.ssl.key') && config.get('server.ssl.cert'); + + // not using https? well that's easy! + if (!useSsl) { + server.connection(connectionOptions); + return; + } + + server.connection({ + ...connectionOptions, + tls: true, + listener: httpolyglot.createServer({ + key: readFileSync(config.get('server.ssl.key')), + cert: readFileSync(config.get('server.ssl.cert')), + + ciphers: tlsCiphers, + // We use the server's cipher order rather than the client's to prevent the BEAST attack + honorCipherOrder: true + }) + }); + + server.ext('onRequest', function (req, reply) { + if (req.raw.req.socket.encrypted) { + reply.continue(); + } else { + reply.redirect(formatUrl({ + port, + protocol: 'https', + hostname: host, + pathname: req.url.pathname, + search: req.url.search, + })); + } + }); +} diff --git a/src/server/http/short_url_lookup.js b/src/server/http/short_url_lookup.js new file mode 100644 index 0000000000000..2f0d3b3764486 --- /dev/null +++ b/src/server/http/short_url_lookup.js @@ -0,0 +1,103 @@ +import crypto from 'crypto'; + +export default function (server) { + async function updateMetadata(urlId, urlDoc) { + const client = server.plugins.elasticsearch.client; + const kibanaIndex = server.config().get('kibana.index'); + + try { + await client.update({ + index: kibanaIndex, + type: 'url', + id: urlId, + body: { + doc: { + 'accessDate': new Date(), + 'accessCount': urlDoc._source.accessCount + 1 + } + } + }); + } catch (err) { + server.log('Warning: Error updating url metadata', err); + //swallow errors. It isn't critical if there is no update. + } + } + + async function getUrlDoc(urlId) { + const urlDoc = await new Promise((resolve, reject) => { + const client = server.plugins.elasticsearch.client; + const kibanaIndex = server.config().get('kibana.index'); + + client.get({ + index: kibanaIndex, + type: 'url', + id: urlId + }) + .then(response => { + resolve(response); + }) + .catch(err => { + resolve(); + }); + }); + + return urlDoc; + } + + async function createUrlDoc(url, urlId) { + const newUrlId = await new Promise((resolve, reject) => { + const client = server.plugins.elasticsearch.client; + const kibanaIndex = server.config().get('kibana.index'); + + client.index({ + index: kibanaIndex, + type: 'url', + id: urlId, + body: { + url, + 'accessCount': 0, + 'createDate': new Date(), + 'accessDate': new Date() + } + }) + .then(response => { + resolve(response._id); + }) + .catch(err => { + reject(err); + }); + }); + + return newUrlId; + } + + function createUrlId(url) { + const urlId = crypto.createHash('md5') + .update(url) + .digest('hex'); + + return urlId; + } + + return { + async generateUrlId(url) { + const urlId = createUrlId(url); + const urlDoc = await getUrlDoc(urlId); + if (urlDoc) return urlId; + + return createUrlDoc(url, urlId); + }, + async getUrl(urlId) { + try { + const urlDoc = await getUrlDoc(urlId); + if (!urlDoc) throw new Error('Requested shortened url does not exist in kibana index'); + + updateMetadata(urlId, urlDoc); + + return urlDoc._source.url; + } catch (err) { + return '/'; + } + } + }; +}; diff --git a/src/server/http/tls_ciphers.js b/src/server/http/tls_ciphers.js new file mode 100644 index 0000000000000..b2662f4c870da --- /dev/null +++ b/src/server/http/tls_ciphers.js @@ -0,0 +1,26 @@ +// The default ciphers in node 0.12.x include insecure ciphers, so until +// we enforce a more recent version of node, we craft our own list +// @see https://github.com/nodejs/node/blob/master/src/node_constants.h#L8-L28 +export default [ + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA256', + 'ECDHE-RSA-AES256-SHA384', + 'DHE-RSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA256', + 'DHE-RSA-AES256-SHA256', + 'HIGH', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!SRP', + '!CAMELLIA' +].join(':'); diff --git a/src/server/http/xsrf.js b/src/server/http/xsrf.js index 7b20501481921..a3cf8f583d8e5 100644 --- a/src/server/http/xsrf.js +++ b/src/server/http/xsrf.js @@ -1,19 +1,22 @@ -import { forbidden } from 'boom'; +import { badRequest } from 'boom'; export default function (kbnServer, server, config) { - const token = config.get('server.xsrf.token'); + const version = config.get('pkg.version'); const disabled = config.get('server.xsrf.disableProtection'); - - server.decorate('reply', 'issueXsrfToken', function () { - return token; - }); + const header = 'kbn-version'; server.ext('onPostAuth', function (req, reply) { - if (disabled || req.method === 'get') return reply.continue(); + const noHeaderGet = req.method === 'get' && !req.headers[header]; + if (disabled || noHeaderGet) return reply.continue(); - const attempt = req.headers['kbn-xsrf-token']; - if (!attempt) return reply(forbidden('Missing XSRF token')); - if (attempt !== token) return reply(forbidden('Invalid XSRF token')); + const submission = req.headers[header]; + if (!submission) return reply(badRequest(`Missing ${header} header`)); + if (submission !== version) { + return reply(badRequest('Browser client is out of date, please refresh the page', { + expected: version, + got: submission + })); + } return reply.continue(); }); diff --git a/src/server/kbn_server.js b/src/server/kbn_server.js new file mode 100644 index 0000000000000..8ad9d9c0709d8 --- /dev/null +++ b/src/server/kbn_server.js @@ -0,0 +1,124 @@ +import Hapi from 'hapi'; +import { constant, once, compact, flatten } from 'lodash'; +import { promisify, resolve, fromNode } from 'bluebird'; +import { isWorker } from 'cluster'; +import { fromRoot, pkg } from '../utils'; +import Config from './config/config'; +import loggingConfiguration from './logging/configuration'; + +let rootDir = fromRoot('.'); + +module.exports = class KbnServer { + constructor(settings) { + this.name = pkg.name; + this.version = pkg.version; + this.build = pkg.build || false; + this.rootDir = rootDir; + this.settings = settings || {}; + + this.ready = constant(this.mixin( + require('./config/setup'), // sets this.config, reads this.settings + require('./http'), // sets this.server + require('./logging'), + require('./status'), + + // writes pid file + require('./pid'), + + // find plugins and set this.plugins + require('./plugins/scan'), + + // tell the config we are done loading plugins + require('./config/complete'), + + // setup this.uiExports and this.bundles + require('../ui'), + + // setup server.uiSettings + require('../ui/settings'), + + // ensure that all bundles are built, or that the + // lazy bundle server is running + require('../optimize'), + + // finally, initialize the plugins + require('./plugins/initialize'), + + () => { + if (this.config.get('server.autoListen')) { + this.ready = constant(resolve()); + return this.listen(); + } + } + )); + + this.listen = once(this.listen); + } + + /** + * Extend the KbnServer outside of the constraits of a plugin. This allows access + * to APIs that are not exposed (intentionally) to the plugins and should only + * be used when the code will be kept up to date with Kibana. + * + * @param {...function} - functions that should be called to mixin functionality. + * They are called with the arguments (kibana, server, config) + * and can return a promise to delay execution of the next mixin + * @return {Promise} - promise that is resolved when the final mixin completes. + */ + async mixin(...fns) { + for (let fn of compact(flatten(fns))) { + await fn.call(this, this, this.server, this.config); + } + } + + /** + * Tell the server to listen for incoming requests, or get + * a promise that will be resolved once the server is listening. + * + * @return undefined + */ + async listen() { + let { server, config } = this; + + await this.ready(); + await fromNode(cb => server.start(cb)); + + if (isWorker) { + // help parent process know when we are ready + process.send(['WORKER_LISTENING']); + } + + server.log(['listening', 'info'], `Server running at ${server.info.uri}`); + return server; + } + + async close() { + await fromNode(cb => this.server.stop(cb)); + } + + async inject(opts) { + if (!this.server) await this.ready(); + + return await fromNode(cb => { + try { + this.server.inject(opts, (resp) => { + cb(null, resp); + }); + } catch (err) { + cb(err); + } + }); + } + + applyLoggingConfiguration(settings) { + const config = Config.withDefaultSchema(settings); + const loggingOptions = loggingConfiguration(config); + const subset = { + ops: config.get('ops'), + logging: config.get('logging') + }; + const plain = JSON.stringify(subset, null, 2); + this.server.log(['info', 'config'], 'New logging configuration:\n' + plain); + this.server.plugins['even-better'].monitor.reconfigure(loggingOptions); + } +}; diff --git a/src/server/lib/commanderExtensions.js b/src/server/lib/commanderExtensions.js deleted file mode 100644 index f1e76b4bb1710..0000000000000 --- a/src/server/lib/commanderExtensions.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (program) { - function isCommand(val) { - return typeof val === 'object' && val._name; - } - - program.isCommandSpecified = function () { - return program.args.some(isCommand); - }; -}; diff --git a/src/server/lib/commander_extensions.js b/src/server/lib/commander_extensions.js new file mode 100644 index 0000000000000..abbd76e3be85d --- /dev/null +++ b/src/server/lib/commander_extensions.js @@ -0,0 +1,9 @@ +export default function (program) { + function isCommand(val) { + return typeof val === 'object' && val._name; + } + + program.isCommandSpecified = function () { + return program.args.some(isCommand); + }; +}; diff --git a/src/server/logging/LogFormat.js b/src/server/logging/LogFormat.js deleted file mode 100644 index 18832199fa91a..0000000000000 --- a/src/server/logging/LogFormat.js +++ /dev/null @@ -1,140 +0,0 @@ -let Stream = require('stream'); -let moment = require('moment'); -let _ = require('lodash'); -let numeral = require('@spalger/numeral'); -let ansicolors = require('ansicolors'); -let stringify = require('json-stringify-safe'); -let querystring = require('querystring'); -let inspect = require('util').inspect; -let applyFiltersToKeys = require('./applyFiltersToKeys'); - -function serializeError(err) { - return { - message: err.message, - name: err.name, - stack: err.stack, - code: err.code, - signal: err.signal - }; -} - -let levelColor = function (code) { - if (code < 299) return ansicolors.green(code); - if (code < 399) return ansicolors.yellow(code); - if (code < 499) return ansicolors.magenta(code); - return ansicolors.red(code); -}; - - -module.exports = class TransformObjStream extends Stream.Transform { - constructor(config) { - super({ - readableObjectMode: false, - writableObjectMode: true - }); - this.config = config; - } - - filter(data) { - if (!this.config.filter) return data; - return applyFiltersToKeys(data, this.config.filter); - } - - _transform(event, enc, next) { - var data = this.filter(this.readEvent(event)); - this.push(this.format(data) + '\n'); - next(); - } - - readEvent(event) { - var data = { - type: event.event, - '@timestamp': moment.utc(event.timestamp).format(), - tags: [].concat(event.tags || []), - pid: event.pid - }; - - if (data.type === 'response') { - _.defaults(data, _.pick(event, [ - 'method', - 'statusCode' - ])); - - data.req = { - url: event.path, - method: event.method, - headers: event.headers, - remoteAddress: event.source.remoteAddress, - userAgent: event.source.remoteAddress, - referer: event.source.referer - }; - - var contentLength = 0; - if (typeof event.responsePayload === 'object') { - contentLength = stringify(event.responsePayload).length; - } else { - contentLength = String(event.responsePayload).length; - } - - data.res = { - statusCode: event.statusCode, - responseTime: event.responseTime, - contentLength: contentLength - }; - - var query = querystring.stringify(event.query); - if (query) data.req.url += '?' + query; - - - data.message = data.req.method.toUpperCase() + ' '; - data.message += data.req.url; - data.message += ' '; - data.message += levelColor(data.res.statusCode); - data.message += ' '; - data.message += ansicolors.brightBlack(data.res.responseTime + 'ms'); - data.message += ansicolors.brightBlack(' - ' + numeral(contentLength).format('0.0b')); - } - else if (data.type === 'ops') { - _.defaults(data, _.pick(event, [ - 'pid', - 'os', - 'proc', - 'load' - ])); - data.message = ansicolors.brightBlack('memory: '); - data.message += numeral(data.proc.mem.heapUsed).format('0.0b'); - data.message += ' '; - data.message += ansicolors.brightBlack('uptime: '); - data.message += numeral(data.proc.uptime).format('00:00:00'); - data.message += ' '; - data.message += ansicolors.brightBlack('load: ['); - data.message += data.os.load.map(function (val) { - return numeral(val).format('0.00'); - }).join(' '); - data.message += ansicolors.brightBlack(']'); - data.message += ' '; - data.message += ansicolors.brightBlack('delay: '); - data.message += numeral(data.proc.delay).format('0.000'); - } - else if (data.type === 'error') { - data.level = 'error'; - data.message = event.error.message; - data.error = serializeError(event.error); - data.url = event.url; - } - else if (event.data instanceof Error) { - data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error'; - data.message = event.data.message; - data.error = serializeError(event.data); - } - else if (_.isPlainObject(event.data) && event.data.tmpl) { - _.assign(data, event.data); - data.tmpl = undefined; - data.message = _.template(event.data.tmpl)(event.data); - } - else { - data.message = _.isString(event.data) ? event.data : inspect(event.data); - } - return data; - } -}; diff --git a/src/server/logging/LogFormatJson.js b/src/server/logging/LogFormatJson.js deleted file mode 100644 index f5dd8f3604179..0000000000000 --- a/src/server/logging/LogFormatJson.js +++ /dev/null @@ -1,13 +0,0 @@ -let LogFormat = require('./LogFormat'); -let stringify = require('json-stringify-safe'); - -let stripColors = function (string) { - return string.replace(/\u001b[^m]+m/g, ''); -}; - -module.exports = class KbnLoggerJsonFormat extends LogFormat { - format(data) { - data.message = stripColors(data.message); - return stringify(data); - } -}; diff --git a/src/server/logging/LogFormatString.js b/src/server/logging/LogFormatString.js deleted file mode 100644 index 0360e1914cae9..0000000000000 --- a/src/server/logging/LogFormatString.js +++ /dev/null @@ -1,62 +0,0 @@ -let _ = require('lodash'); -let ansicolors = require('ansicolors'); -let moment = require('moment'); - -let LogFormat = require('./LogFormat'); - -let statuses = [ - 'err', - 'info', - 'error', - 'warning', - 'fatal', - 'status', - 'debug' -]; - -let typeColors = { - log: 'blue', - req: 'green', - res: 'green', - ops: 'cyan', - err: 'red', - info: 'green', - error: 'red', - warning: 'red', - fatal: 'magenta', - status: 'yellow', - debug: 'brightBlack', - server: 'brightBlack', - optmzr: 'white', - optimize: 'magenta', - listening: 'magenta' -}; - -let color = _.memoize(function (name) { - return ansicolors[typeColors[name]] || _.identity; -}); - -let type = _.memoize(function (t) { - return color(t)(_.pad(t, 7).slice(0, 7)); -}); - -let workerType = process.env.kbnWorkerType ? `${type(process.env.kbnWorkerType)} ` : ''; - -module.exports = class KbnLoggerJsonFormat extends LogFormat { - format(data) { - let time = color('time')(moment(data.timestamp).format('HH:mm:ss.SSS')); - let msg = data.error ? color('error')(data.error.stack) : color('message')(data.message); - - let tags = _(data.tags) - .sortBy(function (tag) { - if (color(tag) === _.identity) return `2${tag}`; - if (_.includes(statuses, tag)) return `0${tag}`; - return `1${tag}`; - }) - .reduce(function (s, t) { - return s + `[${ color(t)(t) }]`; - }, ''); - - return `${workerType}${type(data.type)} [${time}] ${tags} ${msg}`; - } -}; diff --git a/src/server/logging/LogReporter.js b/src/server/logging/LogReporter.js deleted file mode 100644 index e108fde23b822..0000000000000 --- a/src/server/logging/LogReporter.js +++ /dev/null @@ -1,34 +0,0 @@ -let _ = require('lodash'); -let Squeeze = require('good-squeeze').Squeeze; -let writeStr = require('fs').createWriteStream; - -let LogFormatJson = require('./LogFormatJson'); -let LogFormatString = require('./LogFormatString'); - -module.exports = class KbnLogger { - constructor(events, config) { - this.squeeze = new Squeeze(events); - this.format = config.json ? new LogFormatJson(config) : new LogFormatString(config); - - if (config.dest === 'stdout') { - this.dest = process.stdout; - } else { - this.dest = writeStr(config.dest, { - flags: 'a', - encoding: 'utf8' - }); - } - } - - init(readstream, emitter, callback) { - - this.output = readstream.pipe(this.squeeze).pipe(this.format); - this.output.pipe(this.dest); - - emitter.on('stop', () => { - this.output.unpipe(this.dest); - }); - - callback(); - } -}; diff --git a/src/server/logging/__tests__/applyFiltersToKeys.js b/src/server/logging/__tests__/applyFiltersToKeys.js deleted file mode 100644 index bb900f3fa2fbc..0000000000000 --- a/src/server/logging/__tests__/applyFiltersToKeys.js +++ /dev/null @@ -1,39 +0,0 @@ -var applyFiltersToKeys = require('../applyFiltersToKeys'); -var expect = require('expect.js'); - -describe('applyFiltersToKeys(obj, actionsByKey)', function () { - it('applies for each key+prop in actionsByKey', function () { - var data = applyFiltersToKeys({ - a: { - b: { - c: 1 - }, - d: { - e: 'foobar' - } - }, - req: { - headers: { - authorization: 'Basic dskd939k2i' - } - } - }, { - b: 'remove', - e: 'censor', - authorization: '/([^\\s]+)$/' - }); - - expect(data).to.eql({ - a: { - d: { - e: 'XXXXXX', - }, - }, - req: { - headers: { - authorization: 'Basic XXXXXXXXXX' - } - } - }); - }); -}); diff --git a/src/server/logging/__tests__/apply_filters_to_keys.js b/src/server/logging/__tests__/apply_filters_to_keys.js new file mode 100644 index 0000000000000..0175b90ff221b --- /dev/null +++ b/src/server/logging/__tests__/apply_filters_to_keys.js @@ -0,0 +1,39 @@ +import applyFiltersToKeys from '../apply_filters_to_keys'; +import expect from 'expect.js'; + +describe('applyFiltersToKeys(obj, actionsByKey)', function () { + it('applies for each key+prop in actionsByKey', function () { + let data = applyFiltersToKeys({ + a: { + b: { + c: 1 + }, + d: { + e: 'foobar' + } + }, + req: { + headers: { + authorization: 'Basic dskd939k2i' + } + } + }, { + b: 'remove', + e: 'censor', + authorization: '/([^\\s]+)$/' + }); + + expect(data).to.eql({ + a: { + d: { + e: 'XXXXXX', + }, + }, + req: { + headers: { + authorization: 'Basic XXXXXXXXXX' + } + } + }); + }); +}); diff --git a/src/server/logging/applyFiltersToKeys.js b/src/server/logging/applyFiltersToKeys.js deleted file mode 100644 index 860bdd38b76f0..0000000000000 --- a/src/server/logging/applyFiltersToKeys.js +++ /dev/null @@ -1,42 +0,0 @@ -function toPojo(obj) { - return JSON.parse(JSON.stringify(obj)); -} - -function replacer(match, group) { - return (new Array(group.length + 1).join('X')); -} - -function apply(obj, key, action) { - for (let k in obj) { - if (obj.hasOwnProperty(k)) { - let val = obj[k]; - if (k === key) { - if (action === 'remove') { - delete obj[k]; - } - else if (action === 'censor' && typeof val === 'object') { - delete obj[key]; - } - else if (action === 'censor') { - obj[k] = ('' + val).replace(/./g, 'X'); - } - else if (/\/.+\//.test(action)) { - var matches = action.match(/\/(.+)\//); - if (matches) { - let regex = new RegExp(matches[1]); - obj[k] = ('' + val).replace(regex, replacer); - } - } - } else if (typeof val === 'object') { - val = apply(val, key, action); - } - } - } - return obj; -} - -module.exports = function (obj, actionsByKey) { - return Object.keys(actionsByKey).reduce((output, key) => { - return apply(output, key, actionsByKey[key]); - }, toPojo(obj)); -}; diff --git a/src/server/logging/apply_filters_to_keys.js b/src/server/logging/apply_filters_to_keys.js new file mode 100644 index 0000000000000..bc85f2fd1f555 --- /dev/null +++ b/src/server/logging/apply_filters_to_keys.js @@ -0,0 +1,42 @@ +function toPojo(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +function replacer(match, group) { + return (new Array(group.length + 1).join('X')); +} + +function apply(obj, key, action) { + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + let val = obj[k]; + if (k === key) { + if (action === 'remove') { + delete obj[k]; + } + else if (action === 'censor' && typeof val === 'object') { + delete obj[key]; + } + else if (action === 'censor') { + obj[k] = ('' + val).replace(/./g, 'X'); + } + else if (/\/.+\//.test(action)) { + let matches = action.match(/\/(.+)\//); + if (matches) { + let regex = new RegExp(matches[1]); + obj[k] = ('' + val).replace(regex, replacer); + } + } + } else if (typeof val === 'object') { + val = apply(val, key, action); + } + } + } + return obj; +} + +module.exports = function (obj, actionsByKey) { + return Object.keys(actionsByKey).reduce((output, key) => { + return apply(output, key, actionsByKey[key]); + }, toPojo(obj)); +}; diff --git a/src/server/logging/configuration.js b/src/server/logging/configuration.js new file mode 100644 index 0000000000000..03fa0d6164a9d --- /dev/null +++ b/src/server/logging/configuration.js @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import logReporter from './log_reporter'; + +export default function loggingConfiguration(config) { + let events = config.get('logging.events'); + + if (config.get('logging.silent')) { + _.defaults(events, {}); + } + else if (config.get('logging.quiet')) { + _.defaults(events, { + log: ['listening', 'error', 'fatal'], + request: ['error'], + error: '*' + }); + } + else if (config.get('logging.verbose')) { + _.defaults(events, { + log: '*', + ops: '*', + request: '*', + response: '*', + error: '*' + }); + } + else { + _.defaults(events, { + log: ['info', 'warning', 'error', 'fatal'], + response: config.get('logging.json') ? '*' : '!', + request: ['info', 'warning', 'error', 'fatal'], + error: '*' + }); + } + + const options = { + opsInterval: config.get('ops.interval'), + requestHeaders: true, + requestPayload: true, + reporters: [ + { + reporter: logReporter, + config: { + json: config.get('logging.json'), + dest: config.get('logging.dest'), + // I'm adding the default here because if you add another filter + // using the commandline it will remove authorization. I want users + // to have to explicitly set --logging.filter.authorization=none to + // have it show up int he logs. + filter: _.defaults(config.get('logging.filter'), { + authorization: 'remove' + }) + }, + events: _.transform(events, function (filtered, val, key) { + // provide a string compatible way to remove events + if (val !== '!') filtered[key] = val; + }, {}) + } + ] + }; + return options; +} diff --git a/src/server/logging/index.js b/src/server/logging/index.js index 2763baa462081..8f4640aadfe03 100644 --- a/src/server/logging/index.js +++ b/src/server/logging/index.js @@ -1,63 +1,15 @@ -let _ = require('lodash'); -let fromNode = require('bluebird').fromNode; +import { fromNode } from 'bluebird'; +import evenBetter from 'even-better'; +import loggingConfiguration from './configuration'; -module.exports = function (kbnServer, server, config) { - return fromNode(function (cb) { - let events = config.get('logging.events'); - - if (config.get('logging.silent')) { - _.defaults(events, {}); - } - else if (config.get('logging.quiet')) { - _.defaults(events, { - log: ['listening', 'error', 'fatal'], - error: '*' - }); - } - else if (config.get('logging.verbose')) { - _.defaults(events, { - log: '*', - ops: '*', - request: '*', - response: '*', - error: '*' - }); - } - else { - _.defaults(events, { - log: ['info', 'warning', 'error', 'fatal'], - response: config.get('logging.json') ? '*' : '!', - error: '*' - }); - } +export default function (kbnServer, server, config) { + // prevent relying on kbnServer so this can be used with other hapi servers + kbnServer = null; + return fromNode(function (cb) { server.register({ - register: require('good'), - options: { - opsInterval: 5000, - requestHeaders: true, - requestPayload: true, - reporters: [ - { - reporter: require('./LogReporter'), - config: { - json: config.get('logging.json'), - dest: config.get('logging.dest'), - // I'm adding the default here because if you add another filter - // using the commandline it will remove authorization. I want users - // to have to explicitly set --logging.filter.authorization=none to - // have it show up int he logs. - filter: _.defaults(config.get('logging.filter'), { - authorization: 'remove' - }) - }, - events: _.transform(events, function (filtered, val, key) { - // provide a string compatible way to remove events - if (val !== '!') filtered[key] = val; - }, {}) - } - ] - } + register: evenBetter, + options: loggingConfiguration(config) }, cb); }); }; diff --git a/src/server/logging/log_format.js b/src/server/logging/log_format.js new file mode 100644 index 0000000000000..7d11c59abd265 --- /dev/null +++ b/src/server/logging/log_format.js @@ -0,0 +1,140 @@ +import Stream from 'stream'; +import moment from 'moment'; +import _ from 'lodash'; +import numeral from '@spalger/numeral'; +import ansicolors from 'ansicolors'; +import stringify from 'json-stringify-safe'; +import querystring from 'querystring'; +import applyFiltersToKeys from './apply_filters_to_keys'; +import { inspect } from 'util'; + +function serializeError(err) { + return { + message: err.message, + name: err.name, + stack: err.stack, + code: err.code, + signal: err.signal + }; +} + +let levelColor = function (code) { + if (code < 299) return ansicolors.green(code); + if (code < 399) return ansicolors.yellow(code); + if (code < 499) return ansicolors.magenta(code); + return ansicolors.red(code); +}; + + +module.exports = class TransformObjStream extends Stream.Transform { + constructor(config) { + super({ + readableObjectMode: false, + writableObjectMode: true + }); + this.config = config; + } + + filter(data) { + if (!this.config.filter) return data; + return applyFiltersToKeys(data, this.config.filter); + } + + _transform(event, enc, next) { + let data = this.filter(this.readEvent(event)); + this.push(this.format(data) + '\n'); + next(); + } + + readEvent(event) { + let data = { + type: event.event, + '@timestamp': moment.utc(event.timestamp).format(), + tags: [].concat(event.tags || []), + pid: event.pid + }; + + if (data.type === 'response') { + _.defaults(data, _.pick(event, [ + 'method', + 'statusCode' + ])); + + data.req = { + url: event.path, + method: event.method, + headers: event.headers, + remoteAddress: event.source.remoteAddress, + userAgent: event.source.remoteAddress, + referer: event.source.referer + }; + + let contentLength = 0; + if (typeof event.responsePayload === 'object') { + contentLength = stringify(event.responsePayload).length; + } else { + contentLength = String(event.responsePayload).length; + } + + data.res = { + statusCode: event.statusCode, + responseTime: event.responseTime, + contentLength: contentLength + }; + + let query = querystring.stringify(event.query); + if (query) data.req.url += '?' + query; + + + data.message = data.req.method.toUpperCase() + ' '; + data.message += data.req.url; + data.message += ' '; + data.message += levelColor(data.res.statusCode); + data.message += ' '; + data.message += ansicolors.brightBlack(data.res.responseTime + 'ms'); + data.message += ansicolors.brightBlack(' - ' + numeral(contentLength).format('0.0b')); + } + else if (data.type === 'ops') { + _.defaults(data, _.pick(event, [ + 'pid', + 'os', + 'proc', + 'load' + ])); + data.message = ansicolors.brightBlack('memory: '); + data.message += numeral(data.proc.mem.heapUsed).format('0.0b'); + data.message += ' '; + data.message += ansicolors.brightBlack('uptime: '); + data.message += numeral(data.proc.uptime).format('00:00:00'); + data.message += ' '; + data.message += ansicolors.brightBlack('load: ['); + data.message += data.os.load.map(function (val) { + return numeral(val).format('0.00'); + }).join(' '); + data.message += ansicolors.brightBlack(']'); + data.message += ' '; + data.message += ansicolors.brightBlack('delay: '); + data.message += numeral(data.proc.delay).format('0.000'); + } + else if (data.type === 'error') { + data.level = 'error'; + data.message = event.error.message; + data.error = serializeError(event.error); + data.url = event.url; + } + else if (event.data instanceof Error) { + data.level = _.contains(event.tags, 'fatal') ? 'fatal' : 'error'; + data.message = event.data.message; + data.error = serializeError(event.data); + } + else if (_.isPlainObject(event.data) && event.data.tmpl) { + _.assign(data, event.data); + data.tmpl = undefined; + data.message = _.template(event.data.tmpl)(event.data); + } + else { + data.message = _.isString(event.data) ? event.data : inspect(event.data); + } + return data; + } +}; diff --git a/src/server/logging/log_format_json.js b/src/server/logging/log_format_json.js new file mode 100644 index 0000000000000..7df528ab968bb --- /dev/null +++ b/src/server/logging/log_format_json.js @@ -0,0 +1,13 @@ +import LogFormat from './log_format'; +import stringify from 'json-stringify-safe'; + +let stripColors = function (string) { + return string.replace(/\u001b[^m]+m/g, ''); +}; + +module.exports = class KbnLoggerJsonFormat extends LogFormat { + format(data) { + data.message = stripColors(data.message); + return stringify(data); + } +}; diff --git a/src/server/logging/log_format_string.js b/src/server/logging/log_format_string.js new file mode 100644 index 0000000000000..189738a1b4c52 --- /dev/null +++ b/src/server/logging/log_format_string.js @@ -0,0 +1,64 @@ +import _ from 'lodash'; +import ansicolors from 'ansicolors'; +import moment from 'moment'; + +import LogFormat from './log_format'; + +let statuses = [ + 'err', + 'info', + 'error', + 'warning', + 'fatal', + 'status', + 'debug' +]; + +let typeColors = { + log: 'blue', + req: 'green', + res: 'green', + ops: 'cyan', + config: 'cyan', + err: 'red', + info: 'green', + error: 'red', + warning: 'red', + fatal: 'magenta', + status: 'yellow', + debug: 'brightBlack', + server: 'brightBlack', + optmzr: 'white', + managr: 'green', + optimize: 'magenta', + listening: 'magenta' +}; + +let color = _.memoize(function (name) { + return ansicolors[typeColors[name]] || _.identity; +}); + +let type = _.memoize(function (t) { + return color(t)(_.pad(t, 7).slice(0, 7)); +}); + +let workerType = process.env.kbnWorkerType ? `${type(process.env.kbnWorkerType)} ` : ''; + +module.exports = class KbnLoggerJsonFormat extends LogFormat { + format(data) { + let time = color('time')(moment(data.timestamp).format('HH:mm:ss.SSS')); + let msg = data.error ? color('error')(data.error.stack) : color('message')(data.message); + + let tags = _(data.tags) + .sortBy(function (tag) { + if (color(tag) === _.identity) return `2${tag}`; + if (_.includes(statuses, tag)) return `0${tag}`; + return `1${tag}`; + }) + .reduce(function (s, t) { + return s + `[${ color(t)(t) }]`; + }, ''); + + return `${workerType}${type(data.type)} [${time}] ${tags} ${msg}`; + } +}; diff --git a/src/server/logging/log_reporter.js b/src/server/logging/log_reporter.js new file mode 100644 index 0000000000000..db5c1655902e8 --- /dev/null +++ b/src/server/logging/log_reporter.js @@ -0,0 +1,34 @@ +import _ from 'lodash'; + +import LogFormatJson from './log_format_json'; +import LogFormatString from './log_format_string'; +import { Squeeze } from 'good-squeeze'; +import { createWriteStream as writeStr } from 'fs'; + +module.exports = class KbnLogger { + constructor(events, config) { + this.squeeze = new Squeeze(events); + this.format = config.json ? new LogFormatJson(config) : new LogFormatString(config); + + if (config.dest === 'stdout') { + this.dest = process.stdout; + } else { + this.dest = writeStr(config.dest, { + flags: 'a', + encoding: 'utf8' + }); + } + } + + init(readstream, emitter, callback) { + + this.output = readstream.pipe(this.squeeze).pipe(this.format); + this.output.pipe(this.dest); + + emitter.on('stop', () => { + this.output.unpipe(this.dest); + }); + + callback(); + } +}; diff --git a/src/server/pid/index.js b/src/server/pid/index.js index b93a464d2c466..7d7c1dbdea0fb 100644 --- a/src/server/pid/index.js +++ b/src/server/pid/index.js @@ -1,20 +1,20 @@ -var _ = require('lodash'); -var Boom = require('boom'); -var Promise = require('bluebird'); -var writeFile = Promise.promisify(require('fs').writeFile); -var unlink = require('fs').unlinkSync; +import _ from 'lodash'; +import Boom from 'boom'; +import Promise from 'bluebird'; +import { unlinkSync as unlink } from 'fs'; +let writeFile = Promise.promisify(require('fs').writeFile); module.exports = Promise.method(function (kbnServer, server, config) { - var path = config.get('pid.file'); + let path = config.get('pid.file'); if (!path) return; - var pid = String(process.pid); + let pid = String(process.pid); return writeFile(path, pid, { flag: 'wx' }) .catch(function (err) { if (err.code !== 'EEXIST') throw err; - var log = { + let log = { tmpl: 'pid file already exists at <%= path %>', path: path, pid: pid @@ -36,7 +36,7 @@ module.exports = Promise.method(function (kbnServer, server, config) { pid: pid }); - var clean = _.once(function (code) { + let clean = _.once(function (code) { unlink(path); }); @@ -47,5 +47,9 @@ module.exports = Promise.method(function (kbnServer, server, config) { // resend SIGINT process.kill(process.pid, 'SIGINT'); }); + + process.on('unhandledRejection', function (reason, promise) { + server.log(['warning'], `Detected an unhandled Promise rejection.\n${reason}`); + }); }); }); diff --git a/src/server/plugins/Plugin.js b/src/server/plugins/Plugin.js deleted file mode 100644 index 226f17239a703..0000000000000 --- a/src/server/plugins/Plugin.js +++ /dev/null @@ -1,101 +0,0 @@ -let _ = require('lodash'); -let Joi = require('joi'); -let { attempt, fromNode } = require('bluebird'); -let { resolve } = require('path'); -let { inherits } = require('util'); - -const defaultConfigSchema = Joi.object({ - enabled: Joi.boolean().default(true) -}).default(); - -module.exports = class Plugin { - constructor(kbnServer, path, pkg, opts) { - this.kbnServer = kbnServer; - this.pkg = pkg; - this.path = path; - - this.id = opts.id || pkg.name; - this.uiExportsSpecs = opts.uiExports || {}; - this.requiredIds = opts.require || []; - this.version = opts.version || pkg.version; - this.publicDir = opts.publicDir !== false ? resolve(path, 'public') : null; - this.externalCondition = opts.initCondition || _.constant(true); - this.externalInit = opts.init || _.noop; - this.getConfigSchema = opts.config || _.noop; - this.init = _.once(this.init); - } - - static scoped(kbnServer, path, pkg) { - return class ScopedPlugin extends Plugin { - constructor(opts) { - super(kbnServer, path, pkg, opts || {}); - } - }; - } - - async readConfig() { - let schema = await this.getConfigSchema(Joi); - let { config } = this.kbnServer; - config.extendSchema(this.id, schema || defaultConfigSchema); - - if (config.get([this.id, 'enabled'])) { - return true; - } else { - config.removeSchema(this.id); - return false; - } - } - - async init() { - let { id, version, kbnServer } = this; - let { config } = kbnServer; - - // setup the hapi register function and get on with it - let register = (server, options, next) => { - this.server = server; - - // bind the server and options to all - // apps created by this plugin - for (let app of this.apps) { - app.getInjectedVars = _.partial(app.getInjectedVars, server, options); - } - - server.log(['plugins', 'debug'], { - tmpl: 'Initializing plugin <%= plugin.id %>', - plugin: this - }); - - if (this.publicDir) { - server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir); - } - - this.status = kbnServer.status.create(`plugin:${this.id}`); - server.expose('status', this.status); - - attempt(this.externalInit, [server, options], this).nodeify(next); - }; - - register.attributes = { name: id, version: version }; - - await fromNode(cb => { - kbnServer.server.register({ - register: register, - options: config.has(id) ? config.get(id) : null - }, cb); - }); - - // Only change the plugin status to green if the - // intial status has not been changed - if (this.status.state === 'uninitialized') { - this.status.green('Ready'); - } - } - - toJSON() { - return this.pkg; - } - - toString() { - return `${this.id}@${this.version}`; - } -}; diff --git a/src/server/plugins/PluginApi.js b/src/server/plugins/PluginApi.js deleted file mode 100644 index 068f0deca190f..0000000000000 --- a/src/server/plugins/PluginApi.js +++ /dev/null @@ -1,17 +0,0 @@ -let _ = require('lodash'); -let Plugin = require('./Plugin'); -let { basename, join } = require('path'); - -module.exports = class PluginApi { - constructor(kibana, pluginPath) { - this.config = kibana.config; - this.rootDir = kibana.rootDir; - this.package = require(join(pluginPath, 'package.json')); - this.autoload = require('../../ui/autoload'); - this.Plugin = Plugin.scoped(kibana, pluginPath, this.package); - } - - get uiExports() { - throw new Error('plugin.uiExports is not defined until initialize phase'); - } -}; diff --git a/src/server/plugins/PluginCollection.js b/src/server/plugins/PluginCollection.js deleted file mode 100644 index 255707b962702..0000000000000 --- a/src/server/plugins/PluginCollection.js +++ /dev/null @@ -1,53 +0,0 @@ -let { get, indexBy } = require('lodash'); -let inspect = require('util').inspect; - -let PluginApi = require('./PluginApi'); -let Collection = require('requirefrom')('src')('utils/Collection'); - -let byIdCache = Symbol('byIdCache'); -let pluginApis = Symbol('pluginApis'); - -module.exports = class Plugins extends Collection { - - constructor(kbnServer) { - super(); - this.kbnServer = kbnServer; - this[pluginApis] = new Set(); - } - - async new(path) { - let api = new PluginApi(this.kbnServer, path); - this[pluginApis].add(api); - - let output = [].concat(require(path)(api) || []); - let config = this.kbnServer.config; - - if (!output.length) return; - - // clear the byIdCache - this[byIdCache] = null; - - for (let product of output) { - - if (product instanceof api.Plugin) { - let plugin = product; - this.add(plugin); - - let enabled = await plugin.readConfig(); - if (!enabled) this.delete(plugin); - continue; - } - - throw new TypeError('unexpected plugin export ' + inspect(product)); - } - } - - get byId() { - return this[byIdCache] || (this[byIdCache] = indexBy([...this], 'id')); - } - - getPluginApis() { - return this[pluginApis]; - } - -}; diff --git a/src/server/plugins/__tests__/plugin_init.js b/src/server/plugins/__tests__/plugin_init.js new file mode 100644 index 0000000000000..1a38d334969ec --- /dev/null +++ b/src/server/plugins/__tests__/plugin_init.js @@ -0,0 +1,77 @@ +import {values} from 'lodash'; +import expect from 'expect.js'; +import sinon from 'auto-release-sinon'; +import pluginInit from '../plugin_init'; + +describe('Plugin init', () => { + const getPluginCollection = (plugins) => ({ + byId: plugins, + toArray: () => values(plugins) + }); + + it('should call preInit before init', async () => { + const plugins = { + foo: { + id: 'foo', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: [] + }, + bar: { + id: 'bar', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: [] + }, + baz: { + id: 'baz', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: [] + } + }; + + await pluginInit(getPluginCollection(plugins)); + + expect(plugins.foo.preInit.calledBefore(plugins.foo.init)).to.be.ok(); + expect(plugins.foo.preInit.calledBefore(plugins.bar.init)).to.be.ok(); + expect(plugins.foo.preInit.calledBefore(plugins.baz.init)).to.be.ok(); + + expect(plugins.bar.preInit.calledBefore(plugins.foo.init)).to.be.ok(); + expect(plugins.bar.preInit.calledBefore(plugins.bar.init)).to.be.ok(); + expect(plugins.bar.preInit.calledBefore(plugins.baz.init)).to.be.ok(); + + expect(plugins.baz.preInit.calledBefore(plugins.foo.init)).to.be.ok(); + expect(plugins.baz.preInit.calledBefore(plugins.bar.init)).to.be.ok(); + expect(plugins.baz.preInit.calledBefore(plugins.baz.init)).to.be.ok(); + }); + + it('should call preInits in correct order based on requirements', async () => { + const plugins = { + foo: { + id: 'foo', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar', 'baz'] + }, + bar: { + id: 'bar', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: [] + }, + baz: { + id: 'baz', + init: sinon.spy(), + preInit: sinon.spy(), + requiredIds: ['bar'] + } + }; + + await pluginInit(getPluginCollection(plugins)); + + expect(plugins.bar.preInit.firstCall.calledBefore(plugins.foo.init.firstCall)).to.be.ok(); + expect(plugins.bar.preInit.firstCall.calledBefore(plugins.baz.init.firstCall)).to.be.ok(); + expect(plugins.baz.preInit.firstCall.calledBefore(plugins.foo.init.firstCall)).to.be.ok(); + }); +}); diff --git a/src/server/plugins/initialize.js b/src/server/plugins/initialize.js index 0810718f91254..402b750fbcc03 100644 --- a/src/server/plugins/initialize.js +++ b/src/server/plugins/initialize.js @@ -1,5 +1,6 @@ +import pluginInit from './plugin_init'; + module.exports = async function (kbnServer, server, config) { - let { includes, keys } = require('lodash'); if (!config.get('plugins.initialize')) { server.log(['info'], 'Plugin initialization disabled.'); @@ -17,30 +18,5 @@ module.exports = async function (kbnServer, server, config) { }); - - let path = []; - const initialize = async function (id) { - let plugin = plugins.byId[id]; - - if (includes(path, id)) { - throw new Error(`circular dependencies found: "${path.concat(id).join(' -> ')}"`); - } - - path.push(id); - - for (let reqId of plugin.requiredIds) { - if (!plugins.byId[reqId]) { - throw new Error(`Unmet requirement "${reqId}" for plugin "${id}"`); - } - - await initialize(reqId); - } - - await plugin.init(); - path.pop(); - }; - - for (let {id} of plugins) { - await initialize(id); - } + await pluginInit(plugins); }; diff --git a/src/server/plugins/plugin.js b/src/server/plugins/plugin.js new file mode 100644 index 0000000000000..9b805f0c411b4 --- /dev/null +++ b/src/server/plugins/plugin.js @@ -0,0 +1,167 @@ +import _ from 'lodash'; +import toPath from 'lodash/internal/toPath'; +import Joi from 'joi'; +import Bluebird, { attempt, fromNode } from 'bluebird'; +import { basename, resolve } from 'path'; +import { inherits } from 'util'; + +const extendInitFns = Symbol('extend plugin initialization'); + +const defaultConfigSchema = Joi.object({ + enabled: Joi.boolean().default(true) +}).default(); + +/** + * The server plugin class, used to extend the server + * and add custom behavior. A "scoped" plugin class is + * created by the PluginApi class and provided to plugin + * providers that automatically binds all but the `opts` + * arguments. + * + * @class Plugin + * @param {KbnServer} kbnServer - the KbnServer this plugin + * belongs to. + * @param {String} path - the path from which the plugin hails + * @param {Object} pkg - the value of package.json for the plugin + * @param {Objects} opts - the options for this plugin + * @param {String} [opts.id=pkg.name] - the id for this plugin. + * @param {Object} [opts.uiExports] - a mapping of UiExport types + * to UI modules or metadata about + * the UI module + * @param {Array} [opts.require] - the other plugins that this plugin + * requires. These plugins must exist and + * be enabled for this plugin to function. + * The require'd plugins will also be + * initialized first, in order to make sure + * that dependencies provided by these plugins + * are available + * @param {String} [opts.version=pkg.version] - the version of this plugin + * @param {Function} [opts.init] - A function that will be called to initialize + * this plugin at the appropriate time. + * @param {Function} [opts.configPrefix=this.id] - The prefix to use for configuration + * values in the main configuration service + * @param {Function} [opts.config] - A function that produces a configuration + * schema using Joi, which is passed as its + * first argument. + * @param {String|False} [opts.publicDir=path + '/public'] + * - the public directory for this plugin. The final directory must + * have the name "public", though it can be located somewhere besides + * the root of the plugin. Set this to false to disable exposure of a + * public directory + */ +module.exports = class Plugin { + constructor(kbnServer, path, pkg, opts) { + this.kbnServer = kbnServer; + this.pkg = pkg; + this.path = path; + + this.id = opts.id || pkg.name; + this.uiExportsSpecs = opts.uiExports || {}; + this.requiredIds = opts.require || []; + this.version = opts.version || pkg.version; + this.externalPreInit = opts.preInit || _.noop; + this.externalInit = opts.init || _.noop; + this.configPrefix = opts.configPrefix || this.id; + this.getConfigSchema = opts.config || _.noop; + this.preInit = _.once(this.preInit); + this.init = _.once(this.init); + this[extendInitFns] = []; + + if (opts.publicDir === false) { + this.publicDir = null; + } + else if (!opts.publicDir) { + this.publicDir = resolve(this.path, 'public'); + } + else { + this.publicDir = opts.publicDir; + if (basename(this.publicDir) !== 'public') { + throw new Error(`publicDir for plugin ${this.id} must end with a "public" directory.`); + } + } + } + + static scoped(kbnServer, path, pkg) { + return class ScopedPlugin extends Plugin { + constructor(opts) { + super(kbnServer, path, pkg, opts || {}); + } + }; + } + + async readConfig() { + let schema = await this.getConfigSchema(Joi); + let { config } = this.kbnServer; + config.extendSchema(this.configPrefix, schema || defaultConfigSchema); + + if (config.get([...toPath(this.configPrefix), 'enabled'])) { + return true; + } else { + config.removeSchema(this.configPrefix); + return false; + } + } + + async preInit() { + return await this.externalPreInit(this.kbnServer.server); + } + + async init() { + let { id, version, kbnServer, configPrefix } = this; + let { config } = kbnServer; + + // setup the hapi register function and get on with it + const asyncRegister = async (server, options) => { + this.server = server; + + await Promise.all(this[extendInitFns].map(async fn => { + await fn.call(this, server, options); + })); + + server.log(['plugins', 'debug'], { + tmpl: 'Initializing plugin <%= plugin.toString() %>', + plugin: this + }); + + if (this.publicDir) { + server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir); + } + + this.status = kbnServer.status.create(this); + server.expose('status', this.status); + + return await attempt(this.externalInit, [server, options], this); + }; + + const register = (server, options, next) => { + Bluebird.resolve(asyncRegister(server, options)).nodeify(next); + }; + + register.attributes = { name: id, version: version }; + + await fromNode(cb => { + kbnServer.server.register({ + register: register, + options: config.has(configPrefix) ? config.get(configPrefix) : null + }, cb); + }); + + // Only change the plugin status to green if the + // intial status has not been changed + if (this.status.state === 'uninitialized') { + this.status.green('Ready'); + } + } + + extendInit(fn) { + this[extendInitFns].push(fn); + } + + toJSON() { + return this.pkg; + } + + toString() { + return `${this.id}@${this.version}`; + } +}; diff --git a/src/server/plugins/plugin_api.js b/src/server/plugins/plugin_api.js new file mode 100644 index 0000000000000..bda666f81d543 --- /dev/null +++ b/src/server/plugins/plugin_api.js @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import Plugin from './plugin'; +import { basename, join } from 'path'; + +module.exports = class PluginApi { + constructor(kibana, pluginPath) { + this.config = kibana.config; + this.rootDir = kibana.rootDir; + this.package = require(join(pluginPath, 'package.json')); + this.Plugin = Plugin.scoped(kibana, pluginPath, this.package); + } + + get uiExports() { + throw new Error('plugin.uiExports is not defined until initialize phase'); + } + + get autoload() { + console.warn( + `${this.package.id} accessed the autoload lists which are no longer available via the Plugin API.` + + 'Use the `ui/autoload/*` modules instead.' + ); + + return { + directives: [], + filters: [], + styles: [], + modules: [], + require: [] + }; + } +}; diff --git a/src/server/plugins/plugin_collection.js b/src/server/plugins/plugin_collection.js new file mode 100644 index 0000000000000..6019fd3baf60b --- /dev/null +++ b/src/server/plugins/plugin_collection.js @@ -0,0 +1,53 @@ + +import PluginApi from './plugin_api'; +import { inspect } from 'util'; +import { get, indexBy } from 'lodash'; +import Collection from '../../utils/collection'; + +let byIdCache = Symbol('byIdCache'); +let pluginApis = Symbol('pluginApis'); + +module.exports = class Plugins extends Collection { + + constructor(kbnServer) { + super(); + this.kbnServer = kbnServer; + this[pluginApis] = new Set(); + } + + async new(path) { + let api = new PluginApi(this.kbnServer, path); + this[pluginApis].add(api); + + let output = [].concat(require(path)(api) || []); + let config = this.kbnServer.config; + + if (!output.length) return; + + // clear the byIdCache + this[byIdCache] = null; + + for (let product of output) { + + if (product instanceof api.Plugin) { + let plugin = product; + this.add(plugin); + + let enabled = await plugin.readConfig(); + if (!enabled) this.delete(plugin); + continue; + } + + throw new TypeError('unexpected plugin export ' + inspect(product)); + } + } + + get byId() { + return this[byIdCache] || (this[byIdCache] = indexBy([...this], 'id')); + } + + getPluginApis() { + return this[pluginApis]; + } + +}; diff --git a/src/server/plugins/plugin_init.js b/src/server/plugins/plugin_init.js new file mode 100644 index 0000000000000..8f5e9e3184478 --- /dev/null +++ b/src/server/plugins/plugin_init.js @@ -0,0 +1,35 @@ +import { includes } from 'lodash'; + +export default async (plugins) => { + let path = []; + + const initialize = async function (id, fn) { + let plugin = plugins.byId[id]; + + if (includes(path, id)) { + throw new Error(`circular dependencies found: "${path.concat(id).join(' -> ')}"`); + } + + path.push(id); + + for (let reqId of plugin.requiredIds) { + if (!plugins.byId[reqId]) { + throw new Error(`Unmet requirement "${reqId}" for plugin "${id}"`); + } + + await initialize(reqId, fn); + } + + await plugin[fn](); + path.pop(); + }; + + const collection = plugins.toArray(); + for (let {id} of collection) { + await initialize(id, 'preInit'); + } + + for (let {id} of collection) { + await initialize(id, 'init'); + } +}; diff --git a/src/server/plugins/scan.js b/src/server/plugins/scan.js index 37d07e0537c09..baa5147e44530 100644 --- a/src/server/plugins/scan.js +++ b/src/server/plugins/scan.js @@ -1,12 +1,12 @@ +import _ from 'lodash'; +import { fromNode } from 'bluebird'; +import { readdir, stat } from 'fs'; +import { resolve } from 'path'; +import { each } from 'bluebird'; +import PluginCollection from './plugin_collection'; module.exports = async (kbnServer, server, config) => { - let _ = require('lodash'); - let { fromNode } = require('bluebird'); - let { readdir, stat } = require('fs'); - let { resolve } = require('path'); - let { each } = require('bluebird'); - - var PluginCollection = require('./PluginCollection'); - var plugins = kbnServer.plugins = new PluginCollection(kbnServer); + + let plugins = kbnServer.plugins = new PluginCollection(kbnServer); let scanDirs = [].concat(config.get('plugins.scanDirs') || []); let pluginPaths = [].concat(config.get('plugins.paths') || []); diff --git a/src/server/status/Samples.js b/src/server/status/Samples.js deleted file mode 100644 index 879c93d1f7c18..0000000000000 --- a/src/server/status/Samples.js +++ /dev/null @@ -1,26 +0,0 @@ -var _ = require('lodash'); - -function Samples(max) { - this.vals = {}; - this.max = max || Infinity; - this.length = 0; -} - -Samples.prototype.add = function (sample) { - var vals = this.vals; - var length = this.length = Math.min(this.length + 1, this.max); - - _.forOwn(sample, function (val, name) { - if (val == null) val = null; - - if (!vals[name]) vals[name] = new Array(length); - vals[name].unshift([Date.now(), val]); - vals[name].length = length; - }); -}; - -Samples.prototype.toJSON = function () { - return this.vals; -}; - -module.exports = Samples; diff --git a/src/server/status/ServerStatus.js b/src/server/status/ServerStatus.js deleted file mode 100644 index 60f3f3ee6415c..0000000000000 --- a/src/server/status/ServerStatus.js +++ /dev/null @@ -1,72 +0,0 @@ -let _ = require('lodash'); - -let states = require('./states'); -let Status = require('./Status'); - -module.exports = class ServerStatus { - constructor(server) { - this.server = server; - this._created = {}; - } - - create(name) { - return (this._created[name] = new Status(name, this.server)); - } - - each(fn) { - let self = this; - _.forOwn(self._created, function (status, i, list) { - if (status.state !== 'disabled') { - fn.call(self, status, i, list); - } - }); - } - - get(name) { - return this._created[name]; - } - - getState(name) { - return _.get(this._created, [name, 'state'], 'uninitialized'); - } - - overall() { - var state = _(this._created) - .map(function (status) { - return states.get(status.state); - }) - .sortBy('severity') - .pop(); - - var statuses = _.where(this._created, { state: state.id }); - var since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); - - return { - state: state.id, - title: state.title, - nickname: _.sample(state.nicknames), - icon: state.icon, - since: since, - }; - } - - isGreen() { - return (this.overall().state === 'green'); - } - - notGreen() { - return !this.isGreen(); - } - - toString() { - var overall = this.overall(); - return `${overall.title} – ${overall.nickname}`; - } - - toJSON() { - return { - overall: this.overall(), - statuses: _.values(this._created) - }; - } -}; diff --git a/src/server/status/Status.js b/src/server/status/Status.js deleted file mode 100644 index 430d44d0a114f..0000000000000 --- a/src/server/status/Status.js +++ /dev/null @@ -1,67 +0,0 @@ -let _ = require('lodash'); -let EventEmitter = require('events').EventEmitter; -let states = require('./states'); - -class Status extends EventEmitter { - constructor(name, server) { - super(); - - this.name = name; - this.since = new Date(); - this.state = 'uninitialized'; - this.message = 'uninitialized'; - - this.on('change', function (previous, previousMsg) { - this.since = new Date(); - var tags = ['status', name]; - tags.push(this.state === 'red' ? 'error' : 'info'); - - server.log(tags, { - tmpl: 'Status changed from <%= prevState %> to <%= state %><%= message ? " - " + message : "" %>', - name: name, - state: this.state, - message: this.message, - prevState: previous, - prevMsg: previousMsg - }); - }); - } - - toJSON() { - return { - name: this.name, - state: this.state, - icon: states.get(this.state).icon, - message: this.message, - since: this.since - }; - } -} - -states.all.forEach(function (state) { - Status.prototype[state.id] = function (message) { - if (this.state === 'disabled') return; - - let previous = this.state; - let previousMsg = this.message; - - this.error = null; - this.message = message || state.title; - this.state = state.id; - - if (message instanceof Error) { - this.error = message; - this.message = message.message; - } - - if (previous === this.state && previousMsg === this.message) { - // noop - return; - } - - this.emit(state.id, previous, previousMsg); - this.emit('change', previous, previousMsg); - }; -}); - -module.exports = Status; diff --git a/src/server/status/__tests__/ServerStatus.js b/src/server/status/__tests__/ServerStatus.js deleted file mode 100644 index 08187c0b8bcd6..0000000000000 --- a/src/server/status/__tests__/ServerStatus.js +++ /dev/null @@ -1,88 +0,0 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var sinon = require('sinon'); - -var states = require('../states'); -var Status = require('../Status'); -var ServerStatus = require('../ServerStatus'); - -describe('ServerStatus class', function () { - var server; - var serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), log: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - describe('#create(name)', function () { - it('should create a new status by name', function () { - var status = serverStatus.create('name'); - expect(status).to.be.a(Status); - }); - }); - - describe('#get(name)', function () { - it('exposes plugins by name', function () { - var status = serverStatus.create('name'); - expect(serverStatus.get('name')).to.be(status); - }); - }); - - describe('#getState(name)', function () { - it('should expose the state of the plugin by name', function () { - var status = serverStatus.create('name'); - status.green(); - expect(serverStatus.getState('name')).to.be('green'); - }); - }); - - describe('#overall()', function () { - it('considers each status to produce a summary', function () { - var status = serverStatus.create('name'); - - expect(serverStatus.overall().state).to.be('uninitialized'); - - var match = function (overall, state) { - expect(overall).to.have.property('state', state.id); - expect(overall).to.have.property('title', state.title); - expect(overall).to.have.property('icon', state.icon); - expect(overall).to.have.property('icon', state.icon); - expect(state.nicknames).contain(overall.nickname); - }; - - status.green(); - match(serverStatus.overall(), states.get('green')); - - status.yellow(); - match(serverStatus.overall(), states.get('yellow')); - - status.red(); - match(serverStatus.overall(), states.get('red')); - }); - }); - - - describe('#toJSON()', function () { - it('serializes to overall status and individuals', function () { - var one = serverStatus.create('one'); - var two = serverStatus.create('two'); - var three = serverStatus.create('three'); - - one.green(); - two.yellow(); - three.red(); - - var obj = JSON.parse(JSON.stringify(serverStatus)); - expect(obj).to.have.property('overall'); - expect(obj.overall.state).to.eql(serverStatus.overall().state); - expect(obj.statuses).to.have.length(3); - - var outs = _.indexBy(obj.statuses, 'name'); - expect(outs.one).to.have.property('state', 'green'); - expect(outs.two).to.have.property('state', 'yellow'); - expect(outs.three).to.have.property('state', 'red'); - }); - }); - -}); diff --git a/src/server/status/__tests__/Status.js b/src/server/status/__tests__/Status.js deleted file mode 100644 index 8563500a34af0..0000000000000 --- a/src/server/status/__tests__/Status.js +++ /dev/null @@ -1,101 +0,0 @@ -var expect = require('expect.js'); -var sinon = require('sinon'); -var Status = require('../Status'); -var ServerStatus = require('../ServerStatus'); - -describe('Status class', function () { - - var server; - var serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), log: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - it('should have an "uninitialized" state initially', function () { - expect(serverStatus.create('test')).to.have.property('state', 'uninitialized'); - }); - - it('emits change when the status is set', function (done) { - var status = serverStatus.create('test'); - - status.once('change', function (prev, prevMsg) { - expect(status.state).to.be('green'); - expect(status.message).to.be('GREEN'); - expect(prev).to.be('uninitialized'); - - status.once('change', function (prev, prevMsg) { - expect(status.state).to.be('red'); - expect(status.message).to.be('RED'); - expect(prev).to.be('green'); - expect(prevMsg).to.be('GREEN'); - - done(); - }); - - status.red('RED'); - }); - - status.green('GREEN'); - }); - - it('should only trigger the change listener when something changes', function () { - var status = serverStatus.create('test'); - var stub = sinon.stub(); - status.on('change', stub); - status.green('Ready'); - status.green('Ready'); - status.red('Not Ready'); - sinon.assert.calledTwice(stub); - }); - - it('should create a JSON representation of the status', function () { - var status = serverStatus.create('test'); - status.green('Ready'); - - var json = status.toJSON(); - expect(json.state).to.eql('green'); - expect(json.message).to.eql('Ready'); - }); - - function testState(color) { - it(`should change the state to ${color} when #${color}() is called`, function () { - var status = serverStatus.create('test'); - var message = 'testing ' + color; - status[color](message); - expect(status).to.have.property('state', color); - expect(status).to.have.property('message', message); - }); - - it(`should trigger the "change" listner when #${color}() is called`, function (done) { - var status = serverStatus.create('test'); - var message = 'testing ' + color; - status.on('change', function (prev, prevMsg) { - expect(status.state).to.be(color); - expect(status.message).to.be(message); - - expect(prev).to.be('uninitialized'); - expect(prevMsg).to.be('uninitialized'); - done(); - }); - status[color](message); - }); - - it(`should trigger the "${color}" listner when #${color}() is called`, function (done) { - var status = serverStatus.create('test'); - var message = 'testing ' + color; - status.on(color, function (prev, prevMsg) { - expect(status.state).to.be(color); - expect(status.message).to.be(message); - done(); - }); - status[color](message); - }); - } - - testState('green'); - testState('yellow'); - testState('red'); - -}); diff --git a/src/server/status/__tests__/server_status.js b/src/server/status/__tests__/server_status.js new file mode 100644 index 0000000000000..6c3b627c6981a --- /dev/null +++ b/src/server/status/__tests__/server_status.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import sinon from 'sinon'; + +import states from '../states'; +import Status from '../status'; +import ServerStatus from '../server_status'; + +describe('ServerStatus class', function () { + const plugin = {id: 'name', version: '1.2.3'}; + + let server; + let serverStatus; + + beforeEach(function () { + server = { expose: sinon.stub(), log: sinon.stub() }; + serverStatus = new ServerStatus(server); + }); + + describe('#create(plugin)', function () { + it('should create a new status by plugin', function () { + let status = serverStatus.create(plugin); + expect(status).to.be.a(Status); + }); + }); + + describe('#get(name)', function () { + it('exposes plugins by its id/name', function () { + let status = serverStatus.create(plugin); + expect(serverStatus.get('name')).to.be(status); + }); + }); + + describe('#getState(name)', function () { + it('should expose the state of the plugin by name', function () { + let status = serverStatus.create(plugin); + status.green(); + expect(serverStatus.getState('name')).to.be('green'); + }); + }); + + describe('#overall()', function () { + it('considers each status to produce a summary', function () { + let status = serverStatus.create(plugin); + + expect(serverStatus.overall().state).to.be('uninitialized'); + + let match = function (overall, state) { + expect(overall).to.have.property('state', state.id); + expect(overall).to.have.property('title', state.title); + expect(overall).to.have.property('icon', state.icon); + expect(overall).to.have.property('icon', state.icon); + expect(state.nicknames).contain(overall.nickname); + }; + + status.green(); + match(serverStatus.overall(), states.get('green')); + + status.yellow(); + match(serverStatus.overall(), states.get('yellow')); + + status.red(); + match(serverStatus.overall(), states.get('red')); + }); + }); + + + describe('#toJSON()', function () { + it('serializes to overall status and individuals', function () { + const pluginOne = {id: 'one', version: '1.0.0'}; + const pluginTwo = {id: 'two', version: '2.0.0'}; + const pluginThree = {id: 'three', version: '3.0.0'}; + + let one = serverStatus.create(pluginOne); + let two = serverStatus.create(pluginTwo); + let three = serverStatus.create(pluginThree); + + one.green(); + two.yellow(); + three.red(); + + let obj = JSON.parse(JSON.stringify(serverStatus)); + expect(obj).to.have.property('overall'); + expect(obj.overall.state).to.eql(serverStatus.overall().state); + expect(obj.statuses).to.have.length(3); + + let outs = _.indexBy(obj.statuses, 'name'); + expect(outs.one).to.have.property('state', 'green'); + expect(outs.two).to.have.property('state', 'yellow'); + expect(outs.three).to.have.property('state', 'red'); + }); + }); + +}); diff --git a/src/server/status/__tests__/status.js b/src/server/status/__tests__/status.js new file mode 100644 index 0000000000000..eafeb9f2f7fc6 --- /dev/null +++ b/src/server/status/__tests__/status.js @@ -0,0 +1,132 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import Status from '../status'; +import ServerStatus from '../server_status'; + +describe('Status class', function () { + const plugin = {id: 'test', version: '1.2.3'}; + + let server; + let serverStatus; + + beforeEach(function () { + server = { expose: sinon.stub(), log: sinon.stub() }; + serverStatus = new ServerStatus(server); + }); + + it('should have an "uninitialized" state initially', function () { + expect(serverStatus.create(plugin)).to.have.property('state', 'uninitialized'); + }); + + it('emits change when the status is set', function (done) { + let status = serverStatus.create(plugin); + + status.once('change', function (prev, prevMsg) { + expect(status.state).to.be('green'); + expect(status.message).to.be('GREEN'); + expect(prev).to.be('uninitialized'); + + status.once('change', function (prev, prevMsg) { + expect(status.state).to.be('red'); + expect(status.message).to.be('RED'); + expect(prev).to.be('green'); + expect(prevMsg).to.be('GREEN'); + + done(); + }); + + status.red('RED'); + }); + + status.green('GREEN'); + }); + + it('should only trigger the change listener when something changes', function () { + let status = serverStatus.create(plugin); + let stub = sinon.stub(); + status.on('change', stub); + status.green('Ready'); + status.green('Ready'); + status.red('Not Ready'); + sinon.assert.calledTwice(stub); + }); + + it('should create a JSON representation of the status', function () { + let status = serverStatus.create(plugin); + status.green('Ready'); + + let json = status.toJSON(); + expect(json.name).to.eql(plugin.id); + expect(json.version).to.eql(plugin.version); + expect(json.state).to.eql('green'); + expect(json.message).to.eql('Ready'); + }); + + it('should call on handler if status is already matched', function (done) { + let status = serverStatus.create(plugin); + let msg = 'Test Ready'; + status.green(msg); + + status.on('green', function (prev, prevMsg) { + expect(arguments.length).to.equal(2); + expect(prev).to.be('green'); + expect(prevMsg).to.be(msg); + expect(status.message).to.equal(msg); + done(); + }); + }); + + it('should call once handler if status is already matched', function (done) { + let status = serverStatus.create(plugin); + let msg = 'Test Ready'; + status.green(msg); + + status.once('green', function (prev, prevMsg) { + expect(arguments.length).to.equal(2); + expect(prev).to.be('green'); + expect(prevMsg).to.be(msg); + expect(status.message).to.equal(msg); + done(); + }); + }); + + function testState(color) { + it(`should change the state to ${color} when #${color}() is called`, function () { + let status = serverStatus.create(plugin); + let message = 'testing ' + color; + status[color](message); + expect(status).to.have.property('state', color); + expect(status).to.have.property('message', message); + }); + + it(`should trigger the "change" listner when #${color}() is called`, function (done) { + let status = serverStatus.create(plugin); + let message = 'testing ' + color; + status.on('change', function (prev, prevMsg) { + expect(status.state).to.be(color); + expect(status.message).to.be(message); + + expect(prev).to.be('uninitialized'); + expect(prevMsg).to.be('uninitialized'); + done(); + }); + status[color](message); + }); + + it(`should trigger the "${color}" listner when #${color}() is called`, function (done) { + let status = serverStatus.create(plugin); + let message = 'testing ' + color; + status.on(color, function (prev, prevMsg) { + expect(status.state).to.be(color); + expect(status.message).to.be(message); + done(); + }); + status[color](message); + }); + } + + testState('green'); + testState('yellow'); + testState('red'); + +}); diff --git a/src/server/status/__tests__/wrap_auth_config.js b/src/server/status/__tests__/wrap_auth_config.js new file mode 100644 index 0000000000000..e06e38ae9481c --- /dev/null +++ b/src/server/status/__tests__/wrap_auth_config.js @@ -0,0 +1,42 @@ +import expect from 'expect.js'; +import wrapAuthConfig from '../wrap_auth_config'; + +describe('Status wrapAuthConfig', () => { + let options; + + beforeEach(() => { + options = { + method: 'GET', + path: '/status', + handler: function (request, reply) { + return reply(); + } + }; + }); + + it('should return a function', () => { + expect(wrapAuthConfig()).to.be.a('function'); + expect(wrapAuthConfig(true)).to.be.a('function'); + expect(wrapAuthConfig(false)).to.be.a('function'); + }); + + it('should not add auth config by default', () => { + const wrapAuth = wrapAuthConfig(); + const wrapped = wrapAuth(options); + expect(wrapped).to.not.have.property('config'); + }); + + it('should not add auth config if allowAnonymous is false', () => { + const wrapAuth = wrapAuthConfig(false); + const wrapped = wrapAuth(options); + expect(wrapped).to.not.have.property('config'); + }); + + it('should add auth config if allowAnonymous is true', () => { + const wrapAuth = wrapAuthConfig(true); + const wrapped = wrapAuth(options); + expect(wrapped).to.have.property('config'); + expect(wrapped.config).to.have.property('auth'); + expect(wrapped.config.auth).to.be(false); + }); +}); diff --git a/src/server/status/index.js b/src/server/status/index.js index 16dc436b01369..597401df7e478 100644 --- a/src/server/status/index.js +++ b/src/server/status/index.js @@ -1,37 +1,49 @@ -module.exports = function (kbnServer, server, config) { - var _ = require('lodash'); - var ServerStatus = require('./ServerStatus'); - var { join } = require('path'); +import _ from 'lodash'; +import ServerStatus from './server_status'; +import wrapAuthConfig from './wrap_auth_config'; +import { join } from 'path'; +export default function (kbnServer, server, config) { kbnServer.status = new ServerStatus(kbnServer.server); - if (server.plugins.good) { + if (server.plugins['even-better']) { kbnServer.mixin(require('./metrics')); } - server.route({ + const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); + + server.route(wrapAuth({ method: 'GET', path: '/api/status', handler: function (request, reply) { return reply({ + name: config.get('server.name'), + uuid: config.get('uuid'), status: kbnServer.status.toJSON(), metrics: kbnServer.metrics }); } - }); + })); + + server.decorate('reply', 'renderStatusPage', async function () { + const app = kbnServer.uiExports.getHiddenApp('status_page'); + const response = await getResponse(this); + response.code(kbnServer.status.isGreen() ? 200 : 503); + return response; - server.decorate('reply', 'renderStatusPage', function () { - var app = kbnServer.uiExports.getHiddenApp('statusPage'); - var resp = app ? this.renderApp(app) : this(kbnServer.status.toString()); - resp.code(kbnServer.status.isGreen() ? 200 : 503); - return resp; + function getResponse(ctx) { + if (app) { + return ctx.renderApp(app); + } + return ctx(kbnServer.status.toString()); + } }); - server.route({ + server.route(wrapAuth({ method: 'GET', path: '/status', handler: function (request, reply) { return reply.renderStatusPage(); } - }); + })); }; diff --git a/src/server/status/metrics.js b/src/server/status/metrics.js index 837c0bf01939b..28b99011bc988 100644 --- a/src/server/status/metrics.js +++ b/src/server/status/metrics.js @@ -1,16 +1,16 @@ +import _ from 'lodash'; +import Samples from './samples'; module.exports = function (kbnServer, server, config) { - var _ = require('lodash'); - var Samples = require('./Samples'); let lastReport = Date.now(); kbnServer.metrics = new Samples(12); - server.plugins.good.monitor.on('ops', function (event) { + server.plugins['even-better'].monitor.on('ops', function (event) { let now = Date.now(); let secSinceLast = (now - lastReport) / 1000; lastReport = now; - var port = config.get('server.port'); + let port = config.get('server.port'); let requests = _.get(event, ['requests', port, 'total'], 0); let requestsPerSecond = requests / secSinceLast; diff --git a/src/server/status/samples.js b/src/server/status/samples.js new file mode 100644 index 0000000000000..fd2bc4aa27c84 --- /dev/null +++ b/src/server/status/samples.js @@ -0,0 +1,26 @@ +import _ from 'lodash'; + +function Samples(max) { + this.vals = {}; + this.max = max || Infinity; + this.length = 0; +} + +Samples.prototype.add = function (sample) { + let vals = this.vals; + let length = this.length = Math.min(this.length + 1, this.max); + + _.forOwn(sample, function (val, name) { + if (val == null) val = null; + + if (!vals[name]) vals[name] = new Array(length); + vals[name].unshift([Date.now(), val]); + vals[name].length = length; + }); +}; + +Samples.prototype.toJSON = function () { + return this.vals; +}; + +module.exports = Samples; diff --git a/src/server/status/server_status.js b/src/server/status/server_status.js new file mode 100644 index 0000000000000..99cc6e69fb7a0 --- /dev/null +++ b/src/server/status/server_status.js @@ -0,0 +1,72 @@ +import _ from 'lodash'; + +import states from './states'; +import Status from './status'; + +module.exports = class ServerStatus { + constructor(server) { + this.server = server; + this._created = {}; + } + + create(plugin) { + return (this._created[plugin.id] = new Status(plugin, this.server)); + } + + each(fn) { + let self = this; + _.forOwn(self._created, function (status, i, list) { + if (status.state !== 'disabled') { + fn.call(self, status, i, list); + } + }); + } + + get(name) { + return this._created[name]; + } + + getState(name) { + return _.get(this._created, [name, 'state'], 'uninitialized'); + } + + overall() { + let state = _(this._created) + .map(function (status) { + return states.get(status.state); + }) + .sortBy('severity') + .pop(); + + let statuses = _.where(this._created, { state: state.id }); + let since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); + + return { + state: state.id, + title: state.title, + nickname: _.sample(state.nicknames), + icon: state.icon, + since: since, + }; + } + + isGreen() { + return (this.overall().state === 'green'); + } + + notGreen() { + return !this.isGreen(); + } + + toString() { + let overall = this.overall(); + return `${overall.title} – ${overall.nickname}`; + } + + toJSON() { + return { + overall: this.overall(), + statuses: _.values(this._created) + }; + } +}; diff --git a/src/server/status/states.js b/src/server/status/states.js index 2ad32c2a32928..8c6f3fb90b09d 100644 --- a/src/server/status/states.js +++ b/src/server/status/states.js @@ -1,4 +1,4 @@ -let _ = require('lodash'); +import _ from 'lodash'; exports.all = [ { diff --git a/src/server/status/status.js b/src/server/status/status.js new file mode 100644 index 0000000000000..e19278e6e7884 --- /dev/null +++ b/src/server/status/status.js @@ -0,0 +1,83 @@ +import _ from 'lodash'; +import states from './states'; +import { EventEmitter } from 'events'; + +class Status extends EventEmitter { + constructor(plugin, server) { + super(); + + this.plugin = plugin; + this.since = new Date(); + this.state = 'uninitialized'; + this.message = 'uninitialized'; + + this.on('change', function (previous, previousMsg) { + this.since = new Date(); + let tags = ['status', `plugin:${this.plugin.toString()}`]; + tags.push(this.state === 'red' ? 'error' : 'info'); + + server.log(tags, { + tmpl: 'Status changed from <%= prevState %> to <%= state %><%= message ? " - " + message : "" %>', + state: this.state, + message: this.message, + prevState: previous, + prevMsg: previousMsg + }); + }); + } + + toJSON() { + return { + name: this.plugin.id, + version: this.plugin.version, + state: this.state, + icon: states.get(this.state).icon, + message: this.message, + since: this.since + }; + } + + on(eventName, handler) { + super.on(eventName, handler); + + if (eventName === this.state) { + setImmediate(() => handler(this.state, this.message)); + } + } + + once(eventName, handler) { + if (eventName === this.state) { + setImmediate(() => handler(this.state, this.message)); + } else { + super.once(eventName, handler); + } + } +} + +states.all.forEach(function (state) { + Status.prototype[state.id] = function (message) { + if (this.state === 'disabled') return; + + let previous = this.state; + let previousMsg = this.message; + + this.error = null; + this.message = message || state.title; + this.state = state.id; + + if (message instanceof Error) { + this.error = message; + this.message = message.message; + } + + if (previous === this.state && previousMsg === this.message) { + // noop + return; + } + + this.emit(state.id, previous, previousMsg); + this.emit('change', previous, previousMsg); + }; +}); + +module.exports = Status; diff --git a/src/server/status/wrap_auth_config.js b/src/server/status/wrap_auth_config.js new file mode 100644 index 0000000000000..f9530987f24d4 --- /dev/null +++ b/src/server/status/wrap_auth_config.js @@ -0,0 +1,6 @@ +import {assign, identity} from 'lodash'; + +export default (allowAnonymous) => { + if (allowAnonymous) return options => assign(options, {config: {auth: false}}); + return identity; +}; diff --git a/src/testUtils/noDigestPromises.js b/src/testUtils/noDigestPromises.js deleted file mode 100644 index 4b57a52f0acee..0000000000000 --- a/src/testUtils/noDigestPromises.js +++ /dev/null @@ -1,38 +0,0 @@ -var Bluebird = require('bluebird'); -require('ui/promises'); - -Bluebird.longStackTraces(); - -/** - * replace the Promise service with Bluebird so that tests - * can use promises without having to call $rootScope.apply() - * - * var noDigestPromises = require('testUtils/noDigestPromises'); - * - * describe('some module that does complex shit with promises', function () { - * beforeEach(noDigestPromises.activate); - * - * }); - */ - -var active = false; - -require('ui/modules') -.get('kibana') -.config(function ($provide) { - $provide.decorator('Promise', function ($delegate) { - return active ? Bluebird : $delegate; - }); -}); - -function activate() { active = true; } -function deactivate() { active = false; } - -module.exports = { - activate: activate, - deactivate: deactivate, - activateForSuite: function () { - before(activate); - after(deactivate); - } -}; diff --git a/src/testUtils/simulateKeys.js b/src/testUtils/simulateKeys.js deleted file mode 100644 index ae3c6c0738b1b..0000000000000 --- a/src/testUtils/simulateKeys.js +++ /dev/null @@ -1,106 +0,0 @@ -define(function (require) { - var $ = require('jquery'); - var _ = require('lodash'); - var Promise = require('bluebird'); - var keyMap = require('ui/utils/key_map'); - var reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); - var KeyboardEvent = window.KeyboardEvent; - - /** - * Simulate keyboard events in an element. This allows testing the way that - * elements respond to keyboard input. - * - * # sequence style - * keyboard events occur in a sequence, this array of events describe that sequence. - * - * ## event - * an object with a type property, or a string which will be turned into a single press - * - * ## event types - * ### press - * represents a key press - * - `key`: the key for the button pressed - * - `events`: optional list of events that occur before this press completes - * - * ### wait - * represents a pause in a sequence - * - `ms`: the number of milliseconds that the pause takes - * - * ### repeat - * represents a key being repeated because it is held down. Should only exist as a - * sub event of `press` events. - * - `count`: the number of times the repeat occurs - * - * @param {element} $el - jQuery element where events should occur - * @param {[type]} sequence - an array of events - * @async - */ - return function ($el, sequence) { - var modifierState = { - ctrlKey: false, - shiftKey: false, - altKey: false, - metaKey: false - }; - - return doList(_.clone(sequence)); - - function setModifier(key, state) { - var name = key + 'Key'; - if (modifierState.hasOwnProperty(name)) { - modifierState[name] = !!state; - } - } - - function doList(list) { - return Promise.try(function () { - if (!list || !list.length) return; - - var event = list[0]; - if (_.isString(event)) { - event = { type: 'press', key: event }; - } - - switch (event.type) { - case 'press': - return Promise.resolve() - .then(_.partial(fire, 'keydown', event.key)) - .then(_.partial(fire, 'keypress', event.key)) - .then(_.partial(doList, event.events)) - .then(_.partial(fire, 'keyup', event.key)); - - case 'wait': - return Promise.delay(event.ms); - - case 'repeat': - return (function again(remaining) { - if (!remaining) return Promise.resolve(); - remaining = remaining - 1; - return Promise.resolve() - .then(_.partial(fire, 'keydown', event.key, true)) - .then(_.partial(fire, 'keypress', event.key, true)) - .then(_.partial(again, remaining)); - }(event.count)); - - default: - throw new TypeError('invalid event type "' + event.type + '"'); - } - }) - .then(function () { - if (_.size(list) > 1) return doList(list.slice(1)); - }); - } - - function fire(type, key, repeat) { - var keyCode = reverseKeyMap[key]; - if (!keyCode) throw new TypeError('invalid key "' + key + '"'); - - if (type === 'keydown') setModifier(key, true); - if (type === 'keyup') setModifier(key, false); - - var $target = _.isFunction($el) ? $el() : $el; - var $event = new $.Event(type, _.defaults({ keyCode: keyCode }, modifierState)); - $target.trigger($event); - } - }; -}); diff --git a/src/testUtils/stubIndexPattern.js b/src/testUtils/stubIndexPattern.js deleted file mode 100644 index a4338c9b553ed..0000000000000 --- a/src/testUtils/stubIndexPattern.js +++ /dev/null @@ -1,46 +0,0 @@ -define(function (require) { - return function (Private) { - var _ = require('lodash'); - var sinon = require('sinon'); - var Promise = require('bluebird'); - var IndexedArray = require('ui/IndexedArray'); - var IndexPattern = require('ui/index_patterns/_index_pattern'); - var fieldFormats = Private(require('ui/registry/field_formats')); - var flattenHit = Private(require('ui/index_patterns/_flatten_hit')); - var formatHit = require('ui/index_patterns/_format_hit'); - var getComputedFields = require('ui/index_patterns/_get_computed_fields'); - - var Field = Private(require('ui/index_patterns/_field')); - - function StubIndexPattern(pattern, timeField, fields) { - this.id = pattern; - this.popularizeField = sinon.spy(); - this.timeFieldName = timeField; - this.getNonScriptedFields = sinon.spy(); - this.getScriptedFields = sinon.spy(); - this.metaFields = ['_id', '_type', '_source']; - this.fieldFormatMap = {}; - this.routes = IndexPattern.prototype.routes; - - this.toIndexList = _.constant(Promise.resolve([pattern])); - this.getComputedFields = _.bind(getComputedFields, this); - this.flattenHit = flattenHit(this); - this.formatHit = formatHit(this, fieldFormats.getDefaultInstance('string')); - this.formatField = this.formatHit.formatField; - - this._indexFields = function () { - this.fields = new IndexedArray({ - index: ['name'], - group: ['type'], - initialSet: fields.map(function (field) { - return new Field(this, field); - }, this) - }); - }; - - this._indexFields(); - } - - return StubIndexPattern; - }; -}); diff --git a/src/test_utils/no_digest_promises.js b/src/test_utils/no_digest_promises.js new file mode 100644 index 0000000000000..38a4203ab1b45 --- /dev/null +++ b/src/test_utils/no_digest_promises.js @@ -0,0 +1,39 @@ +import Bluebird from 'bluebird'; +import 'ui/promises'; +import uiModules from 'ui/modules'; + +Bluebird.longStackTraces(); + +/** + * replace the Promise service with Bluebird so that tests + * can use promises without having to call $rootScope.apply() + * + * let noDigestPromises = require('test_utils/no_digest_promises'); + * + * describe('some module that does complex shit with promises', function () { + * beforeEach(noDigestPromises.activate); + * + * }); + */ + +let active = false; + +uiModules +.get('kibana') +.config(function ($provide) { + $provide.decorator('Promise', function ($delegate) { + return active ? Bluebird : $delegate; + }); +}); + +function activate() { active = true; } +function deactivate() { active = false; } + +module.exports = { + activate: activate, + deactivate: deactivate, + activateForSuite: function () { + before(activate); + after(deactivate); + } +}; diff --git a/src/test_utils/simulate_keys.js b/src/test_utils/simulate_keys.js new file mode 100644 index 0000000000000..8282d8e5cc9e3 --- /dev/null +++ b/src/test_utils/simulate_keys.js @@ -0,0 +1,104 @@ +import $ from 'jquery'; +import _ from 'lodash'; +import Promise from 'bluebird'; +import keyMap from 'ui/utils/key_map'; +let reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); +let KeyboardEvent = window.KeyboardEvent; + +/** + * Simulate keyboard events in an element. This allows testing the way that + * elements respond to keyboard input. + * + * # sequence style + * keyboard events occur in a sequence, this array of events describe that sequence. + * + * ## event + * an object with a type property, or a string which will be turned into a single press + * + * ## event types + * ### press + * represents a key press + * - `key`: the key for the button pressed + * - `events`: optional list of events that occur before this press completes + * + * ### wait + * represents a pause in a sequence + * - `ms`: the number of milliseconds that the pause takes + * + * ### repeat + * represents a key being repeated because it is held down. Should only exist as a + * sub event of `press` events. + * - `count`: the number of times the repeat occurs + * + * @param {element} $el - jQuery element where events should occur + * @param {[type]} sequence - an array of events + * @async + */ +export default function ($el, sequence) { + let modifierState = { + ctrlKey: false, + shiftKey: false, + altKey: false, + metaKey: false + }; + + return doList(_.clone(sequence)); + + function setModifier(key, state) { + let name = key + 'Key'; + if (modifierState.hasOwnProperty(name)) { + modifierState[name] = !!state; + } + } + + function doList(list) { + return Promise.try(function () { + if (!list || !list.length) return; + + let event = list[0]; + if (_.isString(event)) { + event = { type: 'press', key: event }; + } + + switch (event.type) { + case 'press': + return Promise.resolve() + .then(_.partial(fire, 'keydown', event.key)) + .then(_.partial(fire, 'keypress', event.key)) + .then(_.partial(doList, event.events)) + .then(_.partial(fire, 'keyup', event.key)); + + case 'wait': + return Promise.delay(event.ms); + + case 'repeat': + return (function again(remaining) { + if (!remaining) return Promise.resolve(); + remaining = remaining - 1; + return Promise.resolve() + .then(_.partial(fire, 'keydown', event.key, true)) + .then(_.partial(fire, 'keypress', event.key, true)) + .then(_.partial(again, remaining)); + }(event.count)); + + default: + throw new TypeError('invalid event type "' + event.type + '"'); + } + }) + .then(function () { + if (_.size(list) > 1) return doList(list.slice(1)); + }); + } + + function fire(type, key, repeat) { + let keyCode = reverseKeyMap[key]; + if (!keyCode) throw new TypeError('invalid key "' + key + '"'); + + if (type === 'keydown') setModifier(key, true); + if (type === 'keyup') setModifier(key, false); + + let $target = _.isFunction($el) ? $el() : $el; + let $event = new $.Event(type, _.defaults({ keyCode: keyCode }, modifierState)); + $target.trigger($event); + } +}; diff --git a/src/test_utils/stub_index_pattern.js b/src/test_utils/stub_index_pattern.js new file mode 100644 index 0000000000000..84f33eb229438 --- /dev/null +++ b/src/test_utils/stub_index_pattern.js @@ -0,0 +1,54 @@ +import _ from 'lodash'; +import sinon from 'sinon'; +import Promise from 'bluebird'; +import IndexedArray from 'ui/indexed_array'; +import IndexPattern from 'ui/index_patterns/_index_pattern'; +import formatHit from 'ui/index_patterns/_format_hit'; +import getComputedFields from 'ui/index_patterns/_get_computed_fields'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit'; +import IndexPatternsFieldProvider from 'ui/index_patterns/_field'; +export default function (Private) { + let fieldFormats = Private(RegistryFieldFormatsProvider); + let flattenHit = Private(IndexPatternsFlattenHitProvider); + + let Field = Private(IndexPatternsFieldProvider); + + function StubIndexPattern(pattern, timeField, fields) { + this.id = pattern; + this.popularizeField = sinon.spy(); + this.timeFieldName = timeField; + this.getNonScriptedFields = sinon.spy(); + this.getScriptedFields = sinon.spy(); + this.metaFields = ['_id', '_type', '_source']; + this.fieldFormatMap = {}; + this.routes = IndexPattern.routes; + + this.toIndexList = _.constant(Promise.resolve([pattern])); + this.toDetailedIndexList = _.constant(Promise.resolve([ + { + index: pattern, + min: 0, + max: 1 + } + ])); + this.getComputedFields = _.bind(getComputedFields, this); + this.flattenHit = flattenHit(this); + this.formatHit = formatHit(this, fieldFormats.getDefaultInstance('string')); + this.formatField = this.formatHit.formatField; + + this._indexFields = function () { + this.fields = new IndexedArray({ + index: ['name'], + group: ['type'], + initialSet: fields.map(function (field) { + return new Field(this, field); + }, this) + }); + }; + + this._indexFields(); + } + + return StubIndexPattern; +}; diff --git a/src/ui/UiApp.js b/src/ui/UiApp.js deleted file mode 100644 index 40efd5b0be3e2..0000000000000 --- a/src/ui/UiApp.js +++ /dev/null @@ -1,47 +0,0 @@ -var _ = require('lodash'); -var { join } = require('path'); -var autoload = require('./autoload'); - -class UiApp { - constructor(uiExports, spec) { - this.uiExports = uiExports; - this.spec = spec || {}; - - this.id = this.spec.id; - if (!this.id) { - throw new Error('Every app must specify it\'s id'); - } - - this.main = this.spec.main; - this.title = this.spec.title; - this.description = this.spec.description; - this.icon = this.spec.icon; - this.hidden = this.spec.hidden; - this.autoloadOverrides = this.spec.autoload; - this.templateName = this.spec.templateName || 'uiApp'; - this.url = `${spec.urlBasePath || ''}${this.spec.url || `/app/${this.id}`}`; - - // once this resolves, no reason to run it again - this.getModules = _.once(this.getModules); - - // variables that are injected into the browser, must serialize to JSON - this.getInjectedVars = this.spec.injectVars || _.noop; - } - - getModules() { - return _.chain([ - this.autoloadOverrides || autoload.require, - this.uiExports.find(_.get(this, 'spec.uses', [])), - ]) - .flatten() - .uniq() - .unshift(this.main) - .value(); - } - - toJSON() { - return _.pick(this, ['id', 'title', 'description', 'icon', 'main', 'url']); - } -} - -module.exports = UiApp; diff --git a/src/ui/UiAppCollection.js b/src/ui/UiAppCollection.js deleted file mode 100644 index e4eda77fb0ede..0000000000000 --- a/src/ui/UiAppCollection.js +++ /dev/null @@ -1,45 +0,0 @@ -let _ = require('lodash'); -let UiApp = require('./UiApp'); -let Collection = require('requirefrom')('src')('utils/Collection'); - -let byIdCache = Symbol('byId'); - -module.exports = class UiAppCollection extends Collection { - - constructor(uiExports, parent) { - super(); - - this.uiExports = uiExports; - - if (!parent) { - this.claimedIds = []; - this.hidden = new UiAppCollection(uiExports, this); - } else { - this.claimedIds = parent.claimedIds; - } - - } - - new(spec) { - if (this.hidden && spec.hidden) { - return this.hidden.new(spec); - } - - let app = new UiApp(this.uiExports, spec); - - if (_.includes(this.claimedIds, app.id)) { - throw new Error('Unable to create two apps with the id ' + app.id + '.'); - } else { - this.claimedIds.push(app.id); - } - - this[byIdCache] = null; - this.add(app); - return app; - } - - get byId() { - return this[byIdCache] || (this[byIdCache] = _.indexBy([...this], 'id')); - } - -}; diff --git a/src/ui/UiBundle.js b/src/ui/UiBundle.js deleted file mode 100644 index b610c24b7b4d0..0000000000000 --- a/src/ui/UiBundle.js +++ /dev/null @@ -1,68 +0,0 @@ - -let { join } = require('path'); -let { promisify } = require('bluebird'); -let read = promisify(require('fs').readFile); -let write = promisify(require('fs').writeFile); -let unlink = promisify(require('fs').unlink); -let stat = promisify(require('fs').stat); - -module.exports = class UiBundle { - constructor(opts) { - - opts = opts || {}; - this.id = opts.id; - this.modules = opts.modules; - this.template = opts.template; - this.env = opts.env; - - let pathBase = join(this.env.workingDir, this.id); - this.entryPath = `${pathBase}.entry.js`; - this.outputPath = `${pathBase}.bundle.js`; - - } - - renderContent() { - return this.template({ - env: this.env, - bundle: this - }); - } - - async readEntryFile() { - try { - let content = await read(this.entryPath); - return content.toString('utf8'); - } - catch (e) { - return null; - } - } - - async writeEntryFile() { - return await write(this.entryPath, this.renderContent(), { encoding: 'utf8' }); - } - - async clearBundleFile() { - try { await unlink(this.outputPath); } - catch (e) { return null; } - } - - async checkForExistingOutput() { - try { - await stat(this.outputPath); - return true; - } - catch (e) { - return false; - } - } - - toJSON() { - return { - id: this.id, - modules: this.modules, - entryPath: this.entryPath, - outputPath: this.outputPath - }; - } -}; diff --git a/src/ui/UiBundleCollection.js b/src/ui/UiBundleCollection.js deleted file mode 100644 index 328c03dad1568..0000000000000 --- a/src/ui/UiBundleCollection.js +++ /dev/null @@ -1,104 +0,0 @@ -let { pull, transform, pluck } = require('lodash'); -let { join } = require('path'); -let { resolve, promisify } = require('bluebird'); -let { makeRe } = require('minimatch'); -let rimraf = promisify(require('rimraf')); -let mkdirp = promisify(require('mkdirp')); -let unlink = promisify(require('fs').unlink); -let readdir = promisify(require('fs').readdir); -let readSync = require('fs').readFileSync; - -let UiBundle = require('./UiBundle'); -let appEntryTemplate = require('./appEntryTemplate'); - -class UiBundleCollection { - constructor(bundlerEnv, filter) { - this.each = []; - this.env = bundlerEnv; - this.filter = makeRe(filter || '*', { - noglobstar: true, - noext: true, - matchBase: true - }); - } - - add(bundle) { - if (!(bundle instanceof UiBundle)) { - throw new TypeError('expected bundle to be an instance of UiBundle'); - } - - if (this.filter.test(bundle.id)) { - this.each.push(bundle); - } - } - - addApp(app) { - this.add(new UiBundle({ - id: app.id, - modules: app.getModules(), - template: appEntryTemplate, - env: this.env - })); - } - - desc() { - switch (this.each.length) { - case 0: - return '0 bundles'; - case 1: - return `bundle for ${this.each[0].id}`; - default: - var ids = this.getIds(); - var last = ids.pop(); - var commas = ids.join(', '); - return `bundles for ${commas} and ${last}`; - } - } - - async ensureDir() { - await mkdirp(this.env.workingDir); - } - - async writeEntryFiles() { - await this.ensureDir(); - - for (let bundle of this.each) { - let existing = await bundle.readEntryFile(); - let expected = bundle.renderContent(); - - if (existing !== expected) { - await bundle.writeEntryFile(); - await bundle.clearBundleFile(); - } - } - } - - async getInvalidBundles() { - let invalids = new UiBundleCollection(this.env); - - for (let bundle of this.each) { - let exists = await bundle.checkForExistingOutput(); - if (!exists) { - invalids.add(bundle); - } - } - - return invalids; - } - - toWebpackEntries() { - return transform(this.each, function (entries, bundle) { - entries[bundle.id] = bundle.entryPath; - }, {}); - } - - getIds() { - return pluck(this.each, 'id'); - } - - toJSON() { - return this.each; - } -} - -module.exports = UiBundleCollection; diff --git a/src/ui/UiBundlerEnv.js b/src/ui/UiBundlerEnv.js deleted file mode 100644 index 74a2195d152c5..0000000000000 --- a/src/ui/UiBundlerEnv.js +++ /dev/null @@ -1,167 +0,0 @@ -let { includes, flow, escapeRegExp } = require('lodash'); -let { isString, isArray, isPlainObject, get } = require('lodash'); -let { keys } = require('lodash'); -let fromRoot = require('../utils/fromRoot'); - -let asRegExp = flow( - escapeRegExp, - function (path) { - let last = path.slice(-1); - if (last === '/' || last === '\\') { - // match a directory explicitly - return path + '.*'; - } else { - // match a directory or files or just the absolute path - return path + '(?:\\.js$|$|\\\\|\\/)?'; - } - }, - RegExp -); - -let arr = v => [].concat(v || []); - -module.exports = class UiBundlerEnv { - constructor(workingDir) { - - // the location that bundle entry files and all compiles files will - // be written - this.workingDir = workingDir; - - // the context that the bundler is running in, this is not officially - // used for anything but it is serialized into the entry file to ensure - // that they are invalidated when the context changes - this.context = {}; - - // the plugins that are used to build this environment - // are tracked and embedded into the entry file so that when the - // environment changes we can rebuild the bundles - this.pluginInfo = []; - - // regular expressions which will prevent webpack from parsing the file - this.noParse = [ - /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, - /node_modules[\/\\](mocha|moment)[\/\\]/ - ]; - - // webpack aliases, like require paths, mapping a prefix to a directory - this.aliases = { - ui: fromRoot('src/ui/public'), - testHarness: fromRoot('src/testHarness/public') - }; - - // map of which plugins created which aliases - this.aliasOwners = {}; - - // webpack loaders map loader configuration to regexps - this.loaders = []; - this.postLoaders = []; - } - - consumePlugin(plugin) { - let tag = `${plugin.id}@${plugin.version}`; - if (includes(this.pluginInfo, tag)) return; - - if (plugin.publicDir) { - this.aliases[`plugins/${plugin.id}`] = plugin.publicDir; - } - - this.pluginInfo.push(tag); - } - - exportConsumer(type) { - switch (type) { - case 'loaders': - return (plugin, spec) => { - for (let loader of arr(spec)) this.addLoader(loader); - }; - - case 'postLoaders': - return (plugin, spec) => { - for (let loader of arr(spec)) this.addPostLoader(loader); - }; - - case 'noParse': - return (plugin, spec) => { - for (let re of arr(spec)) this.addNoParse(re); - }; - - case 'modules': - return (plugin, spec) => { - for (let id of keys(spec)) this.addModule(id, spec[id], plugin.id); - }; - } - } - - addContext(key, val) { - this.context[key] = val; - } - - addLoader(loader) { - this.loaders.push(loader); - } - - addPostLoader(loader) { - this.postLoaders.push(loader); - } - - addNoParse(regExp) { - this.noParse.push(regExp); - } - - addModule(id, spec, pluginId) { - this.claim(id, pluginId); - - // configurable via spec - let path; - let parse = true; - let imports = null; - let exports = null; - let expose = null; - - // basic style, just a path - if (isString(spec)) path = spec; - - if (isArray(spec)) { - path = spec[0]; - imports = spec[1]; - exports = spec[2]; - } - - if (isPlainObject(spec)) { - path = spec.path; - parse = get(spec, 'parse', parse); - imports = get(spec, 'imports', imports); - exports = get(spec, 'exports', exports); - expose = get(spec, 'expose', expose); - } - - if (!path) { - throw new TypeError('Invalid spec definition, unable to identify path'); - } - - this.aliases[id] = path; - - let loader = []; - if (imports) { - loader.push(`imports?${imports}`); - } - - if (exports) loader.push(`exports?${exports}`); - if (expose) loader.push(`expose?${expose}`); - if (loader.length) this.loaders.push({ test: asRegExp(path), loader: loader.join('!') }); - if (!parse) this.addNoParse(path); - } - - claim(id, pluginId) { - let owner = pluginId ? `Plugin ${pluginId}` : 'Kibana Server'; - - // TODO(spalger): we could do a lot more to detect colliding module defs - var existingOwner = this.aliasOwners[id] || this.aliasOwners[`${id}$`]; - - if (existingOwner) { - throw new TypeError(`${owner} attempted to override export "${id}" from ${existingOwner}`); - } - - this.aliasOwners[id] = owner; - } -}; diff --git a/src/ui/UiExports.js b/src/ui/UiExports.js deleted file mode 100644 index 06150249076ef..0000000000000 --- a/src/ui/UiExports.js +++ /dev/null @@ -1,119 +0,0 @@ -var _ = require('lodash'); -var minimatch = require('minimatch'); - -var UiAppCollection = require('./UiAppCollection'); - -class UiExports { - constructor({ urlBasePath }) { - this.apps = new UiAppCollection(this); - this.aliases = {}; - this.urlBasePath = urlBasePath; - this.exportConsumer = _.memoize(this.exportConsumer); - this.consumers = []; - this.bundleProviders = []; - } - - consumePlugin(plugin) { - plugin.apps = new UiAppCollection(this); - - var types = _.keys(plugin.uiExportsSpecs); - if (!types) return false; - - var unkown = _.reject(types, this.exportConsumer, this); - if (unkown.length) { - throw new Error('unknown export types ' + unkown.join(', ') + ' in plugin ' + plugin.id); - } - - for (let consumer of this.consumers) { - consumer.consumePlugin && consumer.consumePlugin(plugin); - } - - types.forEach((type) => { - this.exportConsumer(type)(plugin, plugin.uiExportsSpecs[type]); - }); - } - - addConsumer(consumer) { - this.consumers.push(consumer); - } - - exportConsumer(type) { - for (let consumer of this.consumers) { - if (!consumer.exportConsumer) continue; - let fn = consumer.exportConsumer(type); - if (fn) return fn; - } - - switch (type) { - case 'app': - case 'apps': - return (plugin, specs) => { - for (let spec of [].concat(specs || [])) { - let app = this.apps.new(_.defaults({}, spec, { - id: plugin.id, - urlBasePath: this.urlBasePath - })); - plugin.apps.add(app); - } - }; - - case 'visTypes': - case 'fieldFormats': - case 'spyModes': - return (plugin, spec) => { - this.aliases[type] = _.union(this.aliases[type] || [], spec); - }; - - case 'bundle': - return (plugin, spec) => { - this.bundleProviders.push(spec); - }; - - case 'aliases': - return (plugin, specs) => { - _.forOwn(specs, (spec, adhocType) => { - this.aliases[adhocType] = _.union(this.aliases[adhocType] || [], spec); - }); - }; - } - } - - find(patterns) { - var aliases = this.aliases; - var names = _.keys(aliases); - var matcher = _.partialRight(minimatch.filter, { matchBase: true }); - - return _.chain(patterns) - .map(function (pattern) { - var matches = names.filter(matcher(pattern)); - if (!matches.length) { - throw new Error('Unable to find uiExports for pattern ' + pattern); - } - return matches; - }) - .flattenDeep() - .reduce(function (found, name) { - return found.concat(aliases[name]); - }, []) - .value(); - } - - getAllApps() { - let { apps } = this; - return [...apps].concat(...apps.hidden); - } - - getApp(id) { - return this.apps.byId[id]; - } - - getHiddenApp(id) { - return this.apps.hidden.byId[id]; - } - - getBundleProviders() { - return this.bundleProviders; - } -} - -module.exports = UiExports; diff --git a/src/ui/__tests__/fixtures/plugin_async_foo/index.js b/src/ui/__tests__/fixtures/plugin_async_foo/index.js new file mode 100644 index 0000000000000..a0cd8887bbed0 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_async_foo/index.js @@ -0,0 +1,18 @@ +import Bluebird from 'bluebird'; + +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + delay: Joi.number().required(), + shared: Joi.string(), + }).default(); + }, + + uiExports: { + async injectDefaultVars(server, options) { + await Bluebird.delay(options.delay); + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_async_foo/package.json b/src/ui/__tests__/fixtures/plugin_async_foo/package.json new file mode 100644 index 0000000000000..4ad7dda995ca5 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_async_foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_async_foo", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/fixtures/plugin_bar/index.js b/src/ui/__tests__/fixtures/plugin_bar/index.js new file mode 100644 index 0000000000000..59c5556444496 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_bar/index.js @@ -0,0 +1,14 @@ +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + shared: Joi.string() + }).default(); + }, + + uiExports: { + injectDefaultVars(server, options) { + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_bar/package.json b/src/ui/__tests__/fixtures/plugin_bar/package.json new file mode 100644 index 0000000000000..bfc5da0182266 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_bar", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/fixtures/plugin_foo/index.js b/src/ui/__tests__/fixtures/plugin_foo/index.js new file mode 100644 index 0000000000000..59c5556444496 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_foo/index.js @@ -0,0 +1,14 @@ +export default kibana => new kibana.Plugin({ + config(Joi) { + return Joi.object().keys({ + enabled: Joi.boolean().default(true), + shared: Joi.string() + }).default(); + }, + + uiExports: { + injectDefaultVars(server, options) { + return { shared: options.shared }; + } + } +}); diff --git a/src/ui/__tests__/fixtures/plugin_foo/package.json b/src/ui/__tests__/fixtures/plugin_foo/package.json new file mode 100644 index 0000000000000..6a73b89110ae9 --- /dev/null +++ b/src/ui/__tests__/fixtures/plugin_foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "plugin_foo", + "version": "0.0.0" +} diff --git a/src/ui/__tests__/ui_exports.js b/src/ui/__tests__/ui_exports.js new file mode 100644 index 0000000000000..e5f6996af5fee --- /dev/null +++ b/src/ui/__tests__/ui_exports.js @@ -0,0 +1,107 @@ +import expect from 'expect.js'; +import { resolve } from 'path'; + +import UiExports from '../ui_exports'; +import * as kbnTestServer from '../../../test/utils/kbn_server'; + +describe('UiExports', function () { + describe('#find()', function () { + it('finds exports based on the passed export names', function () { + let uiExports = new UiExports({}); + uiExports.aliases.foo = ['a', 'b', 'c']; + uiExports.aliases.bar = ['d', 'e', 'f']; + + expect(uiExports.find(['foo'])).to.eql(['a', 'b', 'c']); + expect(uiExports.find(['bar'])).to.eql(['d', 'e', 'f']); + expect(uiExports.find(['foo', 'bar'])).to.eql(['a', 'b', 'c', 'd', 'e', 'f']); + }); + + it('allows query types that match nothing', function () { + let uiExports = new UiExports({}); + uiExports.aliases.foo = ['a', 'b', 'c']; + + expect(uiExports.find(['foo'])).to.eql(['a', 'b', 'c']); + expect(uiExports.find(['bar'])).to.eql([]); + expect(uiExports.find(['foo', 'bar'])).to.eql(['a', 'b', 'c']); + }); + }); +// + describe('#defaultInjectedVars', function () { + context('two plugins, two sync', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + before(async function () { + kbnServer = kbnTestServer.createServer({ + plugins: { + paths: [ + resolve(__dirname, 'fixtures/plugin_bar'), + resolve(__dirname, 'fixtures/plugin_foo') + ] + }, + + plugin_foo: { + shared: 'foo' + }, + + plugin_bar: { + shared: 'bar' + } + }); + + await kbnServer.ready(); + }); + + after(async function () { + await kbnServer.close(); + }); + + it('merges the two plugins in the order they are loaded', function () { + expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ + shared: 'foo' + }); + }); + }); + + context('two plugins, one async', function () { + this.slow(10000); + this.timeout(60000); + + let kbnServer; + before(async function () { + kbnServer = kbnTestServer.createServer({ + plugins: { + scanDirs: [], + paths: [ + resolve(__dirname, 'fixtures/plugin_async_foo'), + resolve(__dirname, 'fixtures/plugin_foo') + ] + }, + + plugin_async_foo: { + delay: 500, + shared: 'foo' + }, + + plugin_bar: { + shared: 'bar' + } + }); + + await kbnServer.ready(); + }); + + after(async function () { + await kbnServer.close(); + }); + + it('merges the two plugins in the order they are loaded', function () { + // even though plugin_async_foo loads 500ms later, it is still "first" to merge + expect(kbnServer.uiExports.defaultInjectedVars).to.eql({ + shared: 'foo' + }); + }); + }); + }); +}); diff --git a/src/ui/appEntryTemplate.js b/src/ui/appEntryTemplate.js deleted file mode 100644 index 714de7ba07fce..0000000000000 --- a/src/ui/appEntryTemplate.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = function ({env, bundle}) { - - let pluginSlug = env.pluginInfo.sort() - .map(p => ' * - ' + p) - .join('\n'); - - let requires = bundle.modules - .map(m => `require('${m}');`) - .join('\n'); - - return ` -/** - * Test entry file - * - * This is programatically created and updated, do not modify - * - * context: ${JSON.stringify(env.context)} - * includes code from: -${pluginSlug} - * - */ - -require('ui/chrome') -${requires} -require('ui/chrome').bootstrap(/* xoxo */); - -`; - -}; diff --git a/src/ui/app_entry_template.js b/src/ui/app_entry_template.js new file mode 100644 index 0000000000000..2367d04276d83 --- /dev/null +++ b/src/ui/app_entry_template.js @@ -0,0 +1,29 @@ +module.exports = function ({env, bundle}) { + + const pluginSlug = env.pluginInfo.sort() + .map(p => ' * - ' + p) + .join('\n'); + + const requires = bundle.modules + .map(m => `require('${m}');`) + .join('\n'); + + return ` +/** + * Test entry file + * + * This is programatically created and updated, do not modify + * + * context: ${JSON.stringify(env.context)} + * includes code from: +${pluginSlug} + * + */ + +require('ui/chrome'); +${requires} +require('ui/chrome').bootstrap(/* xoxo */); + +`; + +}; diff --git a/src/ui/autoload.js b/src/ui/autoload.js deleted file mode 100644 index e77111eccd4b7..0000000000000 --- a/src/ui/autoload.js +++ /dev/null @@ -1,82 +0,0 @@ -var _ = require('lodash'); -var resolve = require('path').resolve; -var basename = require('path').basename; -var readdir = require('fs').readdirSync; - -var utils = require('requirefrom')('src/utils'); -var fromRoot = utils('fromRoot'); - -function scan(type) { - var dir = fromRoot('src/ui/public', type); - - return _.chain(readdir(dir)) - .reject(function (name) { - return name[0] === '.' || name[0] === '_'; - }) - .map(function (filename) { - var path = resolve(dir, filename); - var name = basename(filename, '.js'); - return `ui/${type}/${name}`; - }) - .value(); -} - -function findStyles() { - var base = ['ui/styles/theme.less', 'ui/styles/base.less']; - var exclude = ['ui/styles/mixins.less', 'ui/styles/variables.less']; - var found = scan('styles', true); - - return _.difference(_.union(base, found), exclude); -} - -exports.reload = function () { - exports.directives = scan('directives'); - exports.filters = scan('filters'); - exports.styles = findStyles(); - exports.modules = [ - 'angular', - 'ui/chrome', - 'ui/chrome/context', - 'ui/bind', - 'ui/bound_to_config_obj', - 'ui/config', - 'ui/courier', - 'ui/debounce', - 'ui/doc_title', - 'ui/elastic_textarea', - 'ui/es', - 'ui/events', - 'ui/fancy_forms', - 'ui/filter_bar', - 'ui/filter_manager', - 'ui/index_patterns', - 'ui/listen', - 'ui/notify', - 'ui/parse_query', - 'ui/persisted_log', - 'ui/private', - 'ui/promises', - 'ui/safe_confirm', - 'ui/state_management/app_state', - 'ui/state_management/global_state', - 'ui/storage', - 'ui/stringify/register', - 'ui/styleCompile', - 'ui/timefilter', - 'ui/timepicker', - 'ui/tooltip', - 'ui/typeahead', - 'ui/url', - 'ui/validateDateInterval', - 'ui/watch_multi' - ]; - - exports.require = _.flatten([ - exports.directives, - exports.filters, - exports.styles, - exports.modules - ]); -}; - -exports.reload(); diff --git a/src/ui/index.js b/src/ui/index.js index 8f68a018d01ca..ada8fa2a0f069 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,22 +1,23 @@ -module.exports = async (kbnServer, server, config) => { - let { defaults } = require('lodash'); - let Boom = require('boom'); - let formatUrl = require('url').format; - let { resolve } = require('path'); - let readFile = require('fs').readFileSync; +import { format as formatUrl } from 'url'; +import { readFileSync as readFile } from 'fs'; +import { defaults } from 'lodash'; +import Boom from 'boom'; +import { resolve } from 'path'; +import fromRoot from '../utils/from_root'; +import UiExports from './ui_exports'; +import UiBundle from './ui_bundle'; +import UiBundleCollection from './ui_bundle_collection'; +import UiBundlerEnv from './ui_bundler_env'; - let fromRoot = require('../utils/fromRoot'); - let UiExports = require('./UiExports'); - let UiBundle = require('./UiBundle'); - let UiBundleCollection = require('./UiBundleCollection'); - let UiBundlerEnv = require('./UiBundlerEnv'); - let loadingGif = readFile(fromRoot('src/ui/public/loading.gif'), { encoding: 'base64'}); +export default async (kbnServer, server, config) => { - let uiExports = kbnServer.uiExports = new UiExports({ + const loadingGif = readFile(fromRoot('src/ui/public/loading.gif'), { encoding: 'base64'}); + + const uiExports = kbnServer.uiExports = new UiExports({ urlBasePath: config.get('server.basePath') }); - let bundlerEnv = new UiBundlerEnv(config.get('optimize.bundleDir')); + const bundlerEnv = new UiBundlerEnv(config.get('optimize.bundleDir')); bundlerEnv.addContext('env', config.get('env.name')); bundlerEnv.addContext('urlBasePath', config.get('server.basePath')); bundlerEnv.addContext('sourceMaps', config.get('optimize.sourceMaps')); @@ -28,14 +29,14 @@ module.exports = async (kbnServer, server, config) => { uiExports.consumePlugin(plugin); } - let bundles = kbnServer.bundles = new UiBundleCollection(bundlerEnv, config.get('optimize.bundleFilter')); + const bundles = kbnServer.bundles = new UiBundleCollection(bundlerEnv, config.get('optimize.bundleFilter')); for (let app of uiExports.getAllApps()) { bundles.addApp(app); } for (let gen of uiExports.getBundleProviders()) { - let bundle = await gen(UiBundle, bundlerEnv, uiExports.getAllApps()); + const bundle = await gen(UiBundle, bundlerEnv, uiExports.getAllApps(), kbnServer.plugins); if (bundle) bundles.add(bundle); } @@ -47,8 +48,8 @@ module.exports = async (kbnServer, server, config) => { path: '/app/{id}', method: 'GET', handler: function (req, reply) { - let id = req.params.id; - let app = uiExports.apps.byId[id]; + const id = req.params.id; + const app = uiExports.apps.byId[id]; if (!app) return reply(Boom.notFound('Unknown app ' + id)); if (kbnServer.status.isGreen()) { @@ -59,25 +60,22 @@ module.exports = async (kbnServer, server, config) => { } }); - const defaultInjectedVars = {}; - if (config.has('kibana')) { - defaultInjectedVars.kbnIndex = config.get('kibana.index'); - } - if (config.has('elasticsearch')) { - defaultInjectedVars.esShardTimeout = config.get('elasticsearch.shardTimeout'); - defaultInjectedVars.esApiVersion = config.get('elasticsearch.apiVersion'); - } - - server.decorate('reply', 'renderApp', function (app) { + server.decorate('reply', 'renderApp', async function (app) { + const isElasticsearchPluginRed = server.plugins.elasticsearch.status.state === 'red'; + const uiSettings = server.uiSettings(); const payload = { app: app, - nav: uiExports.apps, + nav: uiExports.navLinks.inOrder, version: kbnServer.version, buildNum: config.get('pkg.buildNum'), buildSha: config.get('pkg.buildSha'), basePath: config.get('server.basePath'), - vars: defaults(app.getInjectedVars(), defaultInjectedVars), - xsrfToken: this.issueXsrfToken(), + serverName: config.get('server.name'), + uiSettings: { + defaults: await uiSettings.getDefaults(), + user: isElasticsearchPluginRed ? {} : await uiSettings.getUserProvided() + }, + vars: defaults(app.getInjectedVars() || {}, uiExports.defaultInjectedVars), }; return this.view(app.templateName, { diff --git a/src/ui/public/.eslintrc b/src/ui/public/.eslintrc new file mode 100644 index 0000000000000..b7d7d2a31c13d --- /dev/null +++ b/src/ui/public/.eslintrc @@ -0,0 +1,2 @@ +rules: + no-console: 2 diff --git a/src/ui/public/Binder/Binder.js b/src/ui/public/Binder/Binder.js deleted file mode 100644 index 9f5308a2210d9..0000000000000 --- a/src/ui/public/Binder/Binder.js +++ /dev/null @@ -1,49 +0,0 @@ -var $ = require('jquery'); -var d3 = require('d3'); -var callEach = require('lodash').callEach; -var bindKey = require('lodash').bindKey; -var rest = require('lodash').rest; - -function Binder($scope) { - this.disposal = []; - if ($scope) { - $scope.$on('$destroy', bindKey(this, 'destroy')); - } -} - -Binder.prototype._bind = function (on, off, emitter, args) { - on.apply(emitter, args); - this.disposal.push(function () { - off.apply(emitter, args); - }); -}; - -Binder.prototype.on = function (emitter/*, ...args */) { - this._bind(emitter.on, emitter.off || emitter.removeListener, emitter, rest(arguments)); -}; - -Binder.prototype.jqOn = function (el/*, ...args */) { - var $el = $(el); - this._bind($el.on, $el.off, $el, rest(arguments)); -}; - -Binder.prototype.fakeD3Bind = function (el, event, handler) { - this.jqOn(el, event, function (e) { - // mimick https://github.com/mbostock/d3/blob/3abb00113662463e5c19eb87cd33f6d0ddc23bc0/src/selection/on.js#L87-L94 - var o = d3.event; // Events can be reentrant (e.g., focus). - d3.event = e; - try { - handler.apply(this, [this.__data__]); - } finally { - d3.event = o; - } - }); -}; - -Binder.prototype.destroy = function () { - var destroyers = this.disposal; - this.disposal = []; - callEach(destroyers); -}; - -module.exports = Binder; diff --git a/src/ui/public/Binder/__tests__/Binder.js b/src/ui/public/Binder/__tests__/Binder.js deleted file mode 100644 index c54c053865dd6..0000000000000 --- a/src/ui/public/Binder/__tests__/Binder.js +++ /dev/null @@ -1,67 +0,0 @@ -var $ = require('jquery'); -var sinon = require('auto-release-sinon'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); - -var Binder = require('ui/Binder'); - -describe('Binder class', function () { - var $scope; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($rootScope) { - $scope = $rootScope.$new(); - })); - - context('Constructing with a $scope', function () { - it('accepts a $scope and listens for $destroy', function () { - sinon.stub($scope, '$on'); - var binder = new Binder($scope); - expect($scope.$on.callCount).to.be(1); - expect($scope.$on.args[0][0]).to.be('$destroy'); - }); - - it('unbinds when the $scope is destroyed', function () { - var binder = new Binder($scope); - sinon.stub(binder, 'destroy'); - $scope.$destroy(); - expect(binder.destroy.callCount).to.be(1); - }); - }); - - describe('Binder#on', function () { - it('binds to normal event emitters', function () { - var binder = new Binder(); - var emitter = { - on: sinon.stub(), - removeListener: sinon.stub() - }; - var handler = sinon.stub(); - - binder.on(emitter, 'click', handler); - expect(emitter.on.callCount).to.be(1); - expect(emitter.on.args[0][0]).to.be('click'); - expect(emitter.on.args[0][1]).to.be(handler); - - binder.destroy(); - expect(emitter.removeListener.callCount).to.be(1); - expect(emitter.removeListener.args[0][0]).to.be('click'); - expect(emitter.removeListener.args[0][1]).to.be(handler); - }); - }); - - describe('Binder#jqOn', function () { - it('binds jquery event handlers', function () { - var binder = new Binder(); - var el = document.createElement('div'); - var handler = sinon.stub(); - - binder.jqOn(el, 'click', handler); - $(el).click(); - expect(handler.callCount).to.be(1); - binder.destroy(); - $(el).click(); - expect(handler.callCount).to.be(1); - }); - }); -}); diff --git a/src/ui/public/ConfigTemplate.js b/src/ui/public/ConfigTemplate.js deleted file mode 100644 index 3b8307ab8481a..0000000000000 --- a/src/ui/public/ConfigTemplate.js +++ /dev/null @@ -1,36 +0,0 @@ -define(function (require) { - var _ = require('lodash'); - - function ConfigTemplate(templates) { - var template = this; - template.current = null; - template.toggle = _.partial(update, null); - template.open = _.partial(update, true); - template.close = _.partial(update, false); - - function update(newState, name) { - var toUpdate = templates[name]; - var curState = template.is(name); - if (newState == null) newState = !curState; - - if (newState) { - template.current = toUpdate; - } else { - template.current = null; - } - - return newState; - } - - template.is = function (name) { - return template.current === templates[name]; - }; - - template.toString = function () { - return template.current; - }; - } - - return ConfigTemplate; - -}); diff --git a/src/ui/public/IndexedArray/IndexedArray.js b/src/ui/public/IndexedArray/IndexedArray.js deleted file mode 100644 index e42b67bbf7502..0000000000000 --- a/src/ui/public/IndexedArray/IndexedArray.js +++ /dev/null @@ -1,160 +0,0 @@ -define(function (require) { - - var _ = require('lodash'); - - var inflector = require('ui/IndexedArray/inflector'); - var pathGetter = _(_.get).rearg(1, 0).ary(2); - var inflectIndex = inflector('by'); - var inflectOrder = inflector('in', 'Order'); - - var CLEAR_CACHE = {}; - var OPT_NAMES = IndexedArray.OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable']; - - /** - * Generic extension of Array class, which will index (and reindex) the - * objects it contains based on their properties. - * - * @class IndexedArray - * @module utils - * @constructor - * @param {object} [config] - describes the properties of this registry object - * @param {string[]} [config.index] - a list of props/paths that should be used to index the docs. - * @param {string[]} [config.group] - a list of keys/paths to group docs by. - * @param {string[]} [config.order] - a list of keys/paths to order the keys by. - * @param {object[]} [config.initialSet] - the initial dataset the IndexedArray should contain. - * @param {boolean} [config.immutable] - a flag that hints to people reading the implementation - * that this IndexedArray should not be modified. It's modification - * methods are also removed - */ - _.class(IndexedArray).inherits(Array); - function IndexedArray(config) { - IndexedArray.Super.call(this); - - // just to remind future us that this list is important - config = _.pick(config || {}, OPT_NAMES); - - this.raw = []; - - // setup indices - this._indexNames = _.union( - this._setupIndices(config.group, inflectIndex, _.organizeBy), - this._setupIndices(config.index, inflectIndex, _.indexBy), - this._setupIndices(config.order, inflectOrder, _.sortBy) - ); - - if (config.initialSet) { - this.push.apply(this, config.initialSet); - } - - if (config.immutable) { - // just a hint, bugs caused by updates not propogating would be very - // very very hard to track down - this.push = this.splice = undefined; - } - } - - /** - * Create indices for a group of object properties. getters and setters are used to - * read and control the indices. - * - * @param {string[]} props - the properties that should be used to index docs - * @param {function} inflect - a function that will be called with a property name, and - * creates the public property at which the index will be exposed - * @param {function} op - the function that will be used to create the indices, it is passed - * the raw representaion of the registry, and a getter for reading the - * right prop - * - * @returns {string[]} - the public keys of all indices created - */ - IndexedArray.prototype._setupIndices = function (props, inflect, op) { - // shortcut for empty props - if (!props || props.length === 0) return; - - var self = this; - return props.map(function (prop) { - - var from = pathGetter.partial(prop).value(); - var to = inflect(prop); - var cache; - - Object.defineProperty(self, to, { - enumerable: false, - configurable: false, - - set: function (val) { - // can't set any value other than the CLEAR_CACHE constant - if (val === CLEAR_CACHE) { - cache = false; - } else { - throw new TypeError(to + ' can not be set, it is a computed index of values'); - } - }, - get: function () { - return cache || (cache = op(self.raw, from)); - } - }); - - return to; - }); - - }; - - /** - * (Re)run index/group/order procedures to create indices of - * sub-objects. - * - * @return {undefined} - */ - IndexedArray.prototype._clearIndices = function () { - var self = this; - self._indexNames.forEach(function (name) { - self[name] = CLEAR_CACHE; - }); - }; - - /** - * Copy all array methods which have side-effects, and wrap them - * in a function that will reindex after each call, as well - * as duplex the operation to the .raw version of the IndexedArray. - * - * @param {[type]} method [description] - * @return {[type]} [description] - */ - 'pop push shift splice unshift reverse'.split(' ').forEach(function (method) { - var orig = Array.prototype[method]; - - IndexedArray.prototype[method] = function (/* args... */) { - // call the original method with this context - orig.apply(this, arguments); - - // run the indexers - this._clearIndices(); - - // call the original method on our "raw" array, and return the result(s) - return orig.apply(this.raw, arguments); - }; - }); - - /** - * Remove items from this based on a predicate - * @param {function|object|string} predicate - the predicate used to decide what is removed - * @param {object} context - this binding for predicate - * @return {array} - the removed data - */ - IndexedArray.prototype.remove = function (predicate, context) { - var out = _.remove(this, predicate, context); - _.remove(this.raw, predicate, context); - this._clearIndices(); - return out; - }; - - /** - * provide a hook for the JSON serializer - * @return {array} - a plain, vanilla array with our same data - */ - IndexedArray.prototype.toJSON = function () { - return this.raw; - }; - - return IndexedArray; -}); diff --git a/src/ui/public/IndexedArray/__tests__/IndexedArray.js b/src/ui/public/IndexedArray/__tests__/IndexedArray.js deleted file mode 100644 index c3f19ef00456a..0000000000000 --- a/src/ui/public/IndexedArray/__tests__/IndexedArray.js +++ /dev/null @@ -1,150 +0,0 @@ - -var _ = require('lodash'); -var expect = require('expect.js'); -var IndexedArray = require('ui/IndexedArray'); - -// this is generally a data-structure that IndexedArray is good for managing -var users = [ - { name: 'John', id: 69, username: 'beast', group: 'admins' }, - { name: 'Anon', id: 0, username: 'shhhh', group: 'secret' }, - { name: 'Fern', id: 42, username: 'kitty', group: 'editor' }, - { name: 'Mary', id: 55, username: 'sheep', group: 'editor' } -]; - -// this is how we used to accomplish this, before IndexedArray -users.byName = _.indexBy(users, 'name'); -users.byUsername = _.indexBy(users, 'username'); -users.byGroup = _.groupBy(users, 'group'); -users.inIdOrder = _.sortBy(users, 'id'); - -// then things started becoming unruly... so IndexedArray! - -describe('IndexedArray', function () { - describe('Basics', function () { - var reg; - - beforeEach(function () { - reg = new IndexedArray(); - }); - - it('Extends Array', function () { - expect(reg).to.be.a(Array); - }); - - it('fails basic lodash check', function () { - expect(_.isArray(reg)).to.be(false); - }); - - it('clones to an object', function () { - expect(_.isPlainObject(_.clone(reg))).to.be(true); - expect(_.isArray(_.clone(reg))).to.be(false); - }); - }); - - describe('Indexing', function () { - it('provides the initial set', function () { - var reg = new IndexedArray({ - initialSet: [1, 2, 3] - }); - - expect(reg).to.have.length(3); - - reg.forEach(function (v, i) { - expect(v).to.eql(i + 1); - }); - }); - - it('indexes the initial set', function () { - var reg = new IndexedArray({ - index: ['username'], - initialSet: users - }); - - expect(reg).to.have.property('byUsername'); - expect(reg.byUsername).to.eql(users.byUsername); - }); - - it('updates indices after values are added', function () { - // split up the user list, and add it in chunks - var firstUser = users.slice(0, 1).pop(); - var otherUsers = users.slice(1); - - // start off with all but the first - var reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: otherUsers - }); - - // add the first - reg.push(firstUser); - - // end up with the same structure that is in the users fixture - expect(reg.byGroup).to.eql(users.byGroup); - expect(reg.inIdOrder).to.eql(users.inIdOrder); - }); - - it('updates indices after values are removed', function () { - // start off with all - var reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users - }); - - // remove the last - reg.pop(); - - var expectedCount = users.length - 1; - // indexed lists should be updated - expect(reg).to.have.length(expectedCount); - - var sumOfGroups = _.reduce(reg.byGroup, function (note, group) { - return note + group.length; - }, 0); - expect(sumOfGroups).to.eql(expectedCount); - }); - - it('removes items based on a predicate', function () { - var reg = new IndexedArray({ - group: ['group'], - order: ['id'], - initialSet: users - }); - - reg.remove({name: 'John'}); - - expect(_.eq(reg.raw, reg.slice(0))).to.be(true); - expect(reg.length).to.be(3); - expect(reg[0].name).to.be('Anon'); - }); - - it('updates indices after values are re-ordered', function () { - var rawUsers = users.slice(0); - - // collect and shuffle the ids available - var ids = []; - _.times(rawUsers.length, function (i) { ids.push(i); }); - ids = _.shuffle(ids); - - // move something here - var toI = ids.shift(); - // from here - var fromI = ids.shift(); - // do the move - var move = function (arr) { arr.splice(toI, 0, arr.splice(fromI, 1)[0]); }; - - var reg = new IndexedArray({ - index: ['username'], - initialSet: rawUsers - }); - - var index = reg.byUsername; - - move(reg); - - expect(reg.byUsername).to.eql(index); - expect(reg.byUsername).to.not.be(index); - }); - }); -}); diff --git a/src/ui/public/IndexedArray/__tests__/inflector.js b/src/ui/public/IndexedArray/__tests__/inflector.js deleted file mode 100644 index eb4f04696702a..0000000000000 --- a/src/ui/public/IndexedArray/__tests__/inflector.js +++ /dev/null @@ -1,44 +0,0 @@ -var inflector = require('ui/IndexedArray/inflector'); -var expect = require('expect.js'); - -describe('IndexedArray Inflector', function () { - it('returns a function', function () { - var getter = inflector(); - expect(getter).to.be.a('function'); - }); - - describe('fn', function () { - it('prepends a prefix', function () { - var inflect = inflector('my'); - - expect(inflect('Family')).to.be('myFamily'); - expect(inflect('family')).to.be('myFamily'); - expect(inflect('fAmIlY')).to.be('myFAmIlY'); - }); - - it('adds both a prefix and suffix', function () { - var inflect = inflector('foo', 'Bar'); - - expect(inflect('box')).to.be('fooBoxBar'); - expect(inflect('box.car.MAX')).to.be('fooBoxCarMaxBar'); - expect(inflect('BaZzY')).to.be('fooBaZzYBar'); - }); - - it('ignores prefix if it is already at the end of the inflected string', function () { - var inflect = inflector('foo', 'Bar'); - expect(inflect('fooBox')).to.be('fooBoxBar'); - expect(inflect('FooBox')).to.be('FooBoxBar'); - }); - - it('ignores postfix if it is already at the end of the inflected string', function () { - var inflect = inflector('foo', 'Bar'); - expect(inflect('bar')).to.be('fooBar'); - expect(inflect('showBoxBar')).to.be('fooShowBoxBar'); - }); - - it('works with "name"', function () { - var inflect = inflector('in', 'Order'); - expect(inflect('name')).to.be('inNameOrder'); - }); - }); -}); diff --git a/src/ui/public/IndexedArray/inflector.js b/src/ui/public/IndexedArray/inflector.js deleted file mode 100644 index 38f324274d496..0000000000000 --- a/src/ui/public/IndexedArray/inflector.js +++ /dev/null @@ -1,46 +0,0 @@ -define(function (require) { - - function upFirst(str, total) { - return str.charAt(0).toUpperCase() + (total ? str.substr(1).toLowerCase() : str.substr(1)); - } - - function startsWith(str, test) { - return str.substr(0, test.length).toLowerCase() === test.toLowerCase(); - } - - function endsWith(str, test) { - var tooShort = str.length < test.length; - if (tooShort) return; - - return str.substr(str.length - test.length).toLowerCase() === test.toLowerCase(); - } - - function inflector(prefix, postfix) { - return function inflect(key) { - var inflected; - - if (key.indexOf('.') !== -1) { - inflected = key - .split('.') - .map(function (step, i) { - return (i === 0) ? step : upFirst(step, true); - }) - .join(''); - } else { - inflected = key; - } - - if (prefix && !startsWith(key, prefix)) { - inflected = prefix + upFirst(inflected); - } - - if (postfix && !endsWith(key, postfix)) { - inflected = inflected + postfix; - } - - return inflected; - }; - } - - return inflector; -}); diff --git a/src/ui/public/StackTraceMapper/SourceMapReader.js b/src/ui/public/StackTraceMapper/SourceMapReader.js deleted file mode 100644 index 122a89317d697..0000000000000 --- a/src/ui/public/StackTraceMapper/SourceMapReader.js +++ /dev/null @@ -1,15 +0,0 @@ -var _ = require('lodash'); -var SourceMapConsumer = require('source-map/lib/source-map/source-map-consumer').SourceMapConsumer; -var parse = require('url').parse; - -function SourceMapReader(url, map) { - this.smc = new SourceMapConsumer(map); - this.url = parse(url); - this.re = new RegExp('(^|/)' + _.escapeRegExp(this.url.pathname.slice(1)) + '($|\\?|#)'); -} - -SourceMapReader.prototype.matchUrl = function (stackFileName) { - return this.re.test(stackFileName); -}; - -module.exports = SourceMapReader; diff --git a/src/ui/public/StackTraceMapper/StackTraceMapper.js b/src/ui/public/StackTraceMapper/StackTraceMapper.js deleted file mode 100644 index 0324e1b13aa45..0000000000000 --- a/src/ui/public/StackTraceMapper/StackTraceMapper.js +++ /dev/null @@ -1,65 +0,0 @@ -var _ = require('lodash'); -var $ = require('jquery'); -var resolve = require('bluebird').resolve; -var fetch = require('exports?window.fetch!imports?Promise=bluebird!whatwg-fetch'); - -var setErrorStack = require('./setErrorStack'); -var translateStackLine = require('./translateStackLine'); -var stackLineFormat = require('./stackLineFormat'); -var SourceMapReader = require('./SourceMapReader'); - -function StackTraceMapper() { - this.maps = []; - this.init = _.once(this.init); - this.getMapFor = _.memoize(this.getMapFor); - _.bindAll(this, 'init', 'mapError', 'getMapFor', 'mapLine', 'loadMaps'); -} - -StackTraceMapper.prototype.init = function (mapUrls) { - return this.loadMaps(mapUrls).return(this); -}; - -StackTraceMapper.prototype.mapError = function (err) { - if (!stackLineFormat || !err.stack) return err; - - var stack = err.stack.replace(stackLineFormat, this.mapLine); - return setErrorStack(err, stack); -}; - -StackTraceMapper.prototype.mapLine = function (match, filename, line, col) { - return translateStackLine(this.getMapFor(filename), match, filename, line, col); -}; - -StackTraceMapper.prototype.getMapFor = function (url) { - return _.find(this.maps, function (map) { - return map.matchUrl(url); - }); -}; - -StackTraceMapper.prototype.loadMaps = function (mapUrls) { - mapUrls = _.clone(mapUrls || {}); - - var maps = this.maps; - - $('script[src][src-map]').each(function () { - var $el = $(this); - mapUrls[$el.attr('src')] = $el.attr('src-map'); - }); - - return resolve(_.pairs(mapUrls)) - .map( - _.spread(function (url, mapUrl) { - return fetch(mapUrl) - .then(function (resp) { return resp.json(); }) - .then(function (map) { - maps.push(new SourceMapReader(url, map)); - }); - }) - ); -}; - -StackTraceMapper.getInstance = _.once(function () { - return (new StackTraceMapper()).init(); -}); - -module.exports = StackTraceMapper; diff --git a/src/ui/public/StackTraceMapper/setErrorStack.js b/src/ui/public/StackTraceMapper/setErrorStack.js deleted file mode 100644 index 5ae5e0b553535..0000000000000 --- a/src/ui/public/StackTraceMapper/setErrorStack.js +++ /dev/null @@ -1,34 +0,0 @@ -var _ = require('lodash'); - -var err = new Error(); -try { setByAssignment(err, 'john'); } catch (e) {} // eslint-disable-line - -// err.stack is not always writeable, so we -// do some detection for support and fallback to a -// shadowing method, which "copies" the error but -// keeps the original as the prototype so that -// the error is still an instance of the same -// classes as the original error -if (err.stack === 'john') module.exports = setByAssignment; -else module.exports = setByShadowing; - -function setByShadowing(err, stack) { - var props = _.mapValues(err, function (val) { - return { - enumerable: true, - value: val - }; - }); - - props.stack = { - enumerable: true, - value: stack - }; - - return Object.create(err, props); -} - -function setByAssignment(err, stack) { - err.stack = stack; - return err; -} diff --git a/src/ui/public/StackTraceMapper/stackLineFormat.js b/src/ui/public/StackTraceMapper/stackLineFormat.js deleted file mode 100644 index 27706a165a5c1..0000000000000 --- a/src/ui/public/StackTraceMapper/stackLineFormat.js +++ /dev/null @@ -1,19 +0,0 @@ -var _ = require('lodash'); - -var opts = [ - /@((?:[!#$&-;=?-\[\]_a-z~]|%[0-9a-f]{2})+\.js)\:(\d+)(?:\:(\d+)|())/ig, - /(?: \(|at )((?:[!#$&-;=?-\[\]_a-z~]|%[0-9a-f]{2})+\.js)\:(\d+)(?:\:(\d+)|())/ig -]; - -var sample; -try { throw new Error('msg'); } catch (e) { sample = e.stack; } - -var format = _.find(opts, function (format) { - return format.test(sample); -}); - -if (!format && window.console && window.console.log) { - window.console.log('unable to pick format with stack trace sample ' + sample); -} - -module.exports = format; diff --git a/src/ui/public/StackTraceMapper/translateStackLine.js b/src/ui/public/StackTraceMapper/translateStackLine.js deleted file mode 100644 index de6eae6347b03..0000000000000 --- a/src/ui/public/StackTraceMapper/translateStackLine.js +++ /dev/null @@ -1,41 +0,0 @@ -var _ = require('lodash'); - -module.exports = function (map, match, filename, line, col) { - if (!map) return match; - - var position = { - line: parseFloat(line) || 0, - column: parseFloat(col) || 0 - }; - - var srcPosition = map.smc.originalPositionFor(position); - if (!srcPosition || !srcPosition.source) return match; - - var srcFilename = srcPosition.source; - var srcLine = srcPosition.line; - var srcCol = srcPosition.column; - - if (srcCol === 0 && position.column) { - // TODO: teach sourcemaps correct column - // - // since our bundles are not yet minified we can copy the column - // this won't always be the case - srcCol = position.column; - } - - // fold the components into the original match, so that supporting - // characters (parens, periods, etc) from the format are kept, and so - // we don't accidentally replace the wrong part we use splitting and consumption - var resp = ''; - var remainingResp = match; - var fold = function (replace, replacement) { - var wrappingContent = remainingResp.split(replace); - resp += wrappingContent.shift() + replacement; - remainingResp = wrappingContent.join(replace); - }; - - fold(filename, srcFilename); - fold(line, srcLine); - if (_.isString(col)) fold(col, srcCol); - return resp; -}; diff --git a/src/ui/public/Vis/AggConfig.js b/src/ui/public/Vis/AggConfig.js deleted file mode 100644 index 46ed915a813a8..0000000000000 --- a/src/ui/public/Vis/AggConfig.js +++ /dev/null @@ -1,302 +0,0 @@ -define(function (require) { - return function AggConfigFactory(Private, fieldTypeFilter) { - var _ = require('lodash'); - var fieldFormats = Private(require('ui/registry/field_formats')); - - function AggConfig(vis, opts) { - var self = this; - - self.id = String(opts.id || AggConfig.nextId(vis.aggs)); - self.vis = vis; - self._opts = opts = (opts || {}); - - // setters - self.type = opts.type; - self.schema = opts.schema; - - // resolve the params - self.fillDefaults(opts.params); - } - - /** - * Ensure that all of the objects in the list have ids, the objects - * and list are modified by reference. - * - * @param {array[object]} list - a list of objects, objects can be anything really - * @return {array} - the list that was passed in - */ - AggConfig.ensureIds = function (list) { - var have = []; - var haveNot = []; - list.forEach(function (obj) { - (obj.id ? have : haveNot).push(obj); - }); - - var nextId = AggConfig.nextId(have); - haveNot.forEach(function (obj) { - obj.id = String(nextId++); - }); - - return list; - }; - - /** - * Calculate the next id based on the ids in this list - * - * @return {array} list - a list of objects with id properties - */ - AggConfig.nextId = function (list) { - return 1 + list.reduce(function (max, obj) { - return Math.max(max, +obj.id || 0); - }, 0); - }; - - Object.defineProperties(AggConfig.prototype, { - type: { - get: function () { - return this.__type; - }, - set: function (type) { - if (this.__typeDecorations) { - _.forOwn(this.__typeDecorations, function (prop, name) { - delete this[name]; - }, this); - } - - if (_.isString(type)) { - type = AggConfig.aggTypes.byName[type]; - } - - if (type && _.isFunction(type.decorateAggConfig)) { - this.__typeDecorations = type.decorateAggConfig(); - Object.defineProperties(this, this.__typeDecorations); - } - - this.__type = type; - } - }, - schema: { - get: function () { - return this.__schema; - }, - set: function (schema) { - if (_.isString(schema)) { - schema = this.vis.type.schemas.all.byName[schema]; - } - - this.__schema = schema; - } - } - }); - - /** - * Write the current values to this.params, filling in the defaults as we go - * - * @param {object} [from] - optional object to read values from, - * used when initializing - * @return {undefined} - */ - AggConfig.prototype.fillDefaults = function (from) { - var self = this; - from = from || self.params || {}; - var to = self.params = {}; - - self.getAggParams().forEach(function (aggParam) { - var val = from[aggParam.name]; - - if (val == null) { - if (aggParam.default == null) return; - - if (!_.isFunction(aggParam.default)) { - val = aggParam.default; - } else { - val = aggParam.default(self); - if (val == null) return; - } - } - - if (aggParam.deserialize) { - var isTyped = _.isFunction(aggParam.type); - - var isType = isTyped && (val instanceof aggParam.type); - var isObject = !isTyped && _.isObject(val); - var isDeserialized = (isType || isObject); - - if (!isDeserialized) { - val = aggParam.deserialize(val, self); - } - - to[aggParam.name] = val; - return; - } - - to[aggParam.name] = _.cloneDeep(val); - }); - }; - - /** - * Clear the parameters for this aggConfig - * - * @return {object} the new params object - */ - AggConfig.prototype.resetParams = function () { - var fieldParam = this.type && this.type.params.byName.field; - var field; - - if (fieldParam) { - var prevField = this.params.field; - var fieldOpts = fieldTypeFilter(this.vis.indexPattern.fields, fieldParam.filterFieldTypes); - field = _.contains(fieldOpts, prevField) ? prevField : null; - } - - return this.fillDefaults({ row: this.params.row, field: field }); - }; - - AggConfig.prototype.write = function () { - return this.type.params.write(this); - }; - - AggConfig.prototype.createFilter = function (key) { - if (!_.isFunction(this.type.createFilter)) { - throw new TypeError('The "' + this.type.title + '" aggregation does not support filtering.'); - } - - var field = this.field(); - var label = this.fieldDisplayName(); - if (field && !field.filterable) { - var message = 'The "' + label + '" field can not be used for filtering.'; - if (field.scripted) { - message = 'The "' + label + '" field is scripted and can not be used for filtering.'; - } - throw new TypeError(message); - } - - return this.type.createFilter(this, key); - }; - - /** - * Hook into param onRequest handling, and tell the aggConfig that it - * is being sent to elasticsearc. - * - * @return {[type]} [description] - */ - AggConfig.prototype.requesting = function () { - var self = this; - self.type && self.type.params.forEach(function (param) { - if (param.onRequest) param.onRequest(self); - }); - }; - - /** - * Convert this aggConfig to it's dsl syntax. - * - * Adds params and adhoc subaggs to a pojo, then returns it - * - * @param {AggConfig} aggConfig - the config object to convert - * @return {void|Object} - if the config has a dsl representation, it is - * returned, else undefined is returned - */ - AggConfig.prototype.toDsl = function () { - if (this.type.hasNoDsl) return; - var output = this.write(); - - var configDsl = {}; - configDsl[this.type.dslName || this.type.name] = output.params; - - // if the config requires subAggs, write them to the dsl as well - if (output.subAggs) { - var subDslLvl = configDsl.aggs || (configDsl.aggs = {}); - output.subAggs.forEach(function nestAdhocSubAggs(subAggConfig) { - subDslLvl[subAggConfig.id] = subAggConfig.toDsl(); - }); - } - - return configDsl; - }; - - AggConfig.prototype.toJSON = function () { - var self = this; - var params = self.params; - - var outParams = _.transform(self.getAggParams(), function (out, aggParam) { - var val = params[aggParam.name]; - - // don't serialize undefined/null values - if (val == null) return; - if (aggParam.serialize) val = aggParam.serialize(val, self); - if (val == null) return; - - // to prevent accidental leaking, we will clone all complex values - out[aggParam.name] = _.cloneDeep(val); - }, {}); - - return { - id: self.id, - type: self.type && self.type.name, - schema: self.schema && self.schema.name, - params: outParams - }; - }; - - AggConfig.prototype.getAggParams = function () { - return [].concat( - (this.type) ? this.type.params.raw : [], - (this.schema) ? this.schema.params.raw : [] - ); - }; - - AggConfig.prototype.getResponseAggs = function () { - if (!this.type) return; - return this.type.getResponseAggs(this) || [this]; - }; - - AggConfig.prototype.getValue = function (bucket) { - return this.type.getValue(this, bucket); - }; - - AggConfig.prototype.getKey = function (bucket, key) { - return this.type.getKey(bucket, key, this); - }; - - AggConfig.prototype.makeLabel = function () { - if (!this.type) return ''; - var pre = (_.get(this.vis, 'params.mode') === 'percentage') ? 'Percentage of ' : ''; - return pre += this.type.makeLabel(this); - }; - - AggConfig.prototype.field = function () { - return this.params.field; - }; - - AggConfig.prototype.fieldFormatter = function (contentType, defaultFormat) { - var format = this.type && this.type.getFormat(this); - if (format) return format.getConverterFor(contentType); - return this.fieldOwnFormatter(contentType, defaultFormat); - }; - - AggConfig.prototype.fieldOwnFormatter = function (contentType, defaultFormat) { - var field = this.field(); - var format = field && field.format; - if (!format) format = defaultFormat; - if (!format) format = fieldFormats.getDefaultInstance('string'); - return format.getConverterFor(contentType); - }; - - AggConfig.prototype.fieldName = function () { - var field = this.field(); - return field ? field.name : ''; - }; - - AggConfig.prototype.fieldDisplayName = function () { - var field = this.field(); - return field ? (field.displayName || this.fieldName()) : ''; - }; - - AggConfig.prototype.fieldIsTimeField = function () { - var timeFieldName = this.vis.indexPattern.timeFieldName; - return timeFieldName && this.fieldName() === timeFieldName; - }; - - return AggConfig; - }; -}); diff --git a/src/ui/public/Vis/AggConfigResult.js b/src/ui/public/Vis/AggConfigResult.js deleted file mode 100644 index b7dac72820efa..0000000000000 --- a/src/ui/public/Vis/AggConfigResult.js +++ /dev/null @@ -1,47 +0,0 @@ -define(function () { - var i = 0; - - function AggConfigResult(aggConfig, parent, value, key) { - this.key = key; - this.value = value; - this.aggConfig = aggConfig; - this.$parent = parent; - this.$order = ++i; - - if (aggConfig.schema.group === 'buckets') { - this.type = 'bucket'; - } else { - this.type = 'metric'; - } - } - - /** - * Returns an array of the aggConfigResult and parents up the branch - * @returns {array} Array of aggConfigResults - */ - AggConfigResult.prototype.getPath = function () { - return (function walk(result, path) { - path.unshift(result); - if (result.$parent) return walk(result.$parent, path); - return path; - }(this, [])); - }; - - /** - * Returns an Elasticsearch filter that represents the result. - * @returns {object} Elasticsearch filter - */ - AggConfigResult.prototype.createFilter = function () { - return this.aggConfig.createFilter(this.key); - }; - - AggConfigResult.prototype.toString = function (contentType) { - return this.aggConfig.fieldFormatter(contentType)(this.value); - }; - - AggConfigResult.prototype.valueOf = function () { - return this.value; - }; - - return AggConfigResult; -}); diff --git a/src/ui/public/Vis/AggConfigs.js b/src/ui/public/Vis/AggConfigs.js deleted file mode 100644 index 5a42d9accef10..0000000000000 --- a/src/ui/public/Vis/AggConfigs.js +++ /dev/null @@ -1,148 +0,0 @@ -define(function (require) { - return function AggConfigsFactory(Private) { - var _ = require('lodash'); - var AggConfig = Private(require('ui/Vis/AggConfig')); - var IndexedArray = require('ui/IndexedArray'); - - AggConfig.aggTypes = Private(require('ui/agg_types/index')); - - _.class(AggConfigs).inherits(IndexedArray); - function AggConfigs(vis, configStates) { - var self = this; - self.vis = vis; - - configStates = AggConfig.ensureIds(configStates || []); - - AggConfigs.Super.call(self, { - index: ['id'], - group: ['schema.group', 'type.name', 'schema.name'], - initialSet: configStates.map(function (aggConfigState) { - if (aggConfigState instanceof AggConfig) return aggConfigState; - return new AggConfig(vis, aggConfigState); - }) - }); - - - // Set the defaults for any schema which has them. If the defaults - // for some reason has more then the max only set the max number - // of defaults (not sure why a someone define more... - // but whatever). Also if a schema.name is already set then don't - // set anything. - if (vis && vis.type && vis.type.schemas && vis.type.schemas.all) { - _(vis.type.schemas.all) - .filter(function (schema) { - return _.isArray(schema.defaults) && schema.defaults.length > 0; - }) - .each(function (schema) { - if (!self.bySchemaName[schema.name]) { - var defaults = schema.defaults.slice(0, schema.max); - _.each(defaults, function (defaultState) { - var state = _.defaults({ id: AggConfig.nextId(self) }, defaultState); - self.push(new AggConfig(vis, state)); - }); - } - }) - .commit(); - } - } - - AggConfigs.prototype.toDsl = function () { - var dslTopLvl = {}; - var dslLvlCursor; - var nestedMetrics; - - if (this.vis.isHierarchical()) { - // collect all metrics, and filter out the ones that we won't be copying - nestedMetrics = _(this.vis.aggs.bySchemaGroup.metrics) - .filter(function (agg) { - return agg.type.name !== 'count'; - }) - .map(function (agg) { - return { - config: agg, - dsl: agg.toDsl() - }; - }) - .value(); - } - - this.getRequestAggs() - .filter(function (config) { - return !config.type.hasNoDsl; - }) - .forEach(function nestEachConfig(config, i, list) { - if (!dslLvlCursor) { - // start at the top level - dslLvlCursor = dslTopLvl; - } else { - var prevConfig = list[i - 1]; - var prevDsl = dslLvlCursor[prevConfig.id]; - - // advance the cursor and nest under the previous agg, or - // put it on the same level if the previous agg doesn't accept - // sub aggs - dslLvlCursor = prevDsl.aggs || dslLvlCursor; - } - - var dsl = dslLvlCursor[config.id] = config.toDsl(); - var subAggs; - - if (config.schema.group === 'buckets' && i < list.length - 1) { - // buckets that are not the last item in the list accept sub-aggs - subAggs = dsl.aggs || (dsl.aggs = {}); - } - - if (subAggs && nestedMetrics) { - nestedMetrics.forEach(function (agg) { - subAggs[agg.config.id] = agg.dsl; - }); - } - }); - - return dslTopLvl; - }; - - AggConfigs.prototype.getRequestAggs = function () { - return _.sortBy(this, function (agg) { - return agg.schema.group === 'metrics' ? 1 : 0; - }); - }; - - /** - * Gets the AggConfigs (and possibly ResponseAggConfigs) that - * represent the values that will be produced when all aggs - * are run. - * - * With multi-value metric aggs it is possible for a single agg - * request to result in multiple agg values, which is why the length - * of a vis' responseValuesAggs may be different than the vis' aggs - * - * @return {array[AggConfig]} - */ - AggConfigs.prototype.getResponseAggs = function () { - return this.getRequestAggs().reduce(function (responseValuesAggs, agg) { - var aggs = agg.getResponseAggs(); - return aggs ? responseValuesAggs.concat(aggs) : responseValuesAggs; - }, []); - }; - - - /** - * Find a response agg by it's id. This may be an agg in the aggConfigs, or one - * created specifically for a response value - * - * @param {string} id - the id of the agg to find - * @return {AggConfig} - */ - AggConfigs.prototype.getResponseAggById = function (id) { - id = String(id); - var reqAgg = _.find(this.getRequestAggs(), function (agg) { - return id.substr(0, String(agg.id).length) === agg.id; - }); - if (!reqAgg) return; - return _.find(reqAgg.getResponseAggs(), { id: id }); - }; - - return AggConfigs; - }; -}); diff --git a/src/ui/public/Vis/Renderbot.js b/src/ui/public/Vis/Renderbot.js deleted file mode 100644 index 27ba9d2bd2c53..0000000000000 --- a/src/ui/public/Vis/Renderbot.js +++ /dev/null @@ -1,51 +0,0 @@ -define(function (require) { - var _ = require('lodash'); - - return function RenderbotFactory(Private) { - - /** - * "Abstract" renderbot class which just defines the expected API - * - * @param {Vis} vis - the vis object that contains all configuration data required to render the vis - * @param {jQuery} $el - a jQuery wrapped element to render into - */ - function Renderbot(vis, $el) { - this.vis = vis; - this.$el = $el; - } - - /** - * Each renderbot should implement a #render() method which - * should accept an elasticsearch response and update the underlying visualization - * - * @override - * @param {object} esResp - The raw elasticsearch response - * @return {undefined} - */ - Renderbot.prototype.render = function (esResp) { - throw new Error('not implemented'); - }; - - /** - * Each renderbot should implement the #destroy() method which - * should tear down the owned element, remove event listeners, etc. - * - * @override - * @return {undefined} - */ - Renderbot.prototype.destroy = function () { - throw new Error('not implemented'); - }; - - /** - * Each renderbot can optionally implement the #updateParams() method which - * is used to pass in new vis params. It should not re-render the vis - * - * @override - * @return {undefined} - */ - Renderbot.prototype.updateParams = _.noop; - - return Renderbot; - }; -}); diff --git a/src/ui/public/Vis/Schemas.js b/src/ui/public/Vis/Schemas.js deleted file mode 100644 index d3b4159f985ee..0000000000000 --- a/src/ui/public/Vis/Schemas.js +++ /dev/null @@ -1,62 +0,0 @@ -define(function (require) { - return function VisTypeSchemasFactory(Private) { - var _ = require('lodash'); - var IndexedArray = require('ui/IndexedArray'); - var AggParams = Private(require('ui/agg_types/AggParams')); - - function Schemas(schemas) { - var self = this; - - _(schemas || []) - .map(function (schema) { - if (!schema.name) throw new Error('all schema must have a unique name'); - - if (schema.name === 'split') { - schema.params = [ - { - name: 'row', - default: true - } - ]; - schema.editor = require('plugins/kbn_vislib_vis_types/controls/rows_or_columns.html'); - } else if (schema.name === 'radius') { - schema.editor = require('plugins/kbn_vislib_vis_types/controls/radius_ratio_option.html'); - } - - _.defaults(schema, { - min: 0, - max: Infinity, - group: 'buckets', - title: schema.name, - aggFilter: '*', - editor: false, - params: [] - }); - - // convert the params into a params registry - schema.params = new AggParams(schema.params); - - return schema; - }) - .tap(function (schemas) { - self.all = new IndexedArray({ - index: ['name'], - group: ['group'], - immutable: true, - initialSet: schemas - }); - }) - .groupBy('group') - .forOwn(function (group, groupName) { - self[groupName] = new IndexedArray({ - index: ['name'], - immutable: true, - initialSet: group - }); - }) - .commit(); - } - - return Schemas; - }; -}); diff --git a/src/ui/public/Vis/Vis.js b/src/ui/public/Vis/Vis.js deleted file mode 100644 index 79f7b6bcf5d1b..0000000000000 --- a/src/ui/public/Vis/Vis.js +++ /dev/null @@ -1,126 +0,0 @@ -define(function (require) { - return function VisFactory(Notifier, Private) { - var _ = require('lodash'); - var aggTypes = Private(require('ui/agg_types/index')); - var visTypes = Private(require('ui/registry/vis_types')); - var AggConfigs = Private(require('ui/Vis/AggConfigs')); - - var notify = new Notifier({ - location: 'Vis' - }); - - function Vis(indexPattern, state) { - state = state || {}; - - if (_.isString(state)) { - state = { - type: state - }; - } - - this.indexPattern = indexPattern; - - // http://aphyr.com/data/posts/317/state.gif - this.setState(state); - } - - Vis.convertOldState = function (type, oldState) { - if (!type || _.isString(type)) { - type = visTypes.byName[type || 'histogram']; - } - - var schemas = type.schemas; - - var aggs = _.transform(oldState, function (newConfigs, oldConfigs, oldGroupName) { - var schema = schemas.all.byName[oldGroupName]; - - if (!schema) { - notify.log('unable to match old schema', oldGroupName, 'to a new schema'); - return; - } - - oldConfigs.forEach(function (oldConfig) { - var agg = { - schema: schema.name, - type: oldConfig.agg, - }; - - var aggType = aggTypes.byName[agg.type]; - if (!aggType) { - notify.log('unable to find an agg type for old confg', oldConfig); - return; - } - - agg.params = _.pick(oldConfig, _.keys(aggType.params.byName)); - - newConfigs.push(agg); - }); - }, []); - - return { - type: type, - aggs: aggs - }; - }; - - Vis.prototype.type = 'histogram'; - - Vis.prototype.setState = function (state) { - this.type = state.type || this.type; - if (_.isString(this.type)) this.type = visTypes.byName[this.type]; - - this.listeners = _.assign({}, state.listeners, this.type.listeners); - this.params = _.defaults({}, - _.cloneDeep(state.params || {}), - _.cloneDeep(this.type.params.defaults || {}) - ); - - this.aggs = new AggConfigs(this, state.aggs); - }; - - Vis.prototype.getState = function () { - return { - type: this.type.name, - params: this.params, - aggs: this.aggs.map(function (agg) { - return agg.toJSON(); - }).filter(Boolean), - listeners: this.listeners - }; - }; - - Vis.prototype.createEditableVis = function () { - return this._editableVis || (this._editableVis = this.clone()); - }; - - Vis.prototype.getEditableVis = function () { - return this._editableVis || undefined; - }; - - Vis.prototype.clone = function () { - return new Vis(this.indexPattern, this.getState()); - }; - - Vis.prototype.requesting = function () { - _.invoke(this.aggs.getRequestAggs(), 'requesting'); - }; - - Vis.prototype.isHierarchical = function () { - if (_.isFunction(this.type.hierarchicalData)) { - return !!this.type.hierarchicalData(this); - } else { - return !!this.type.hierarchicalData; - } - }; - - Vis.prototype.hasSchemaAgg = function (schemaName, aggTypeName) { - var aggs = this.aggs.bySchemaName[schemaName] || []; - return aggs.some(function (agg) { - if (!agg.type || !agg.type.name) return false; - return agg.type.name === aggTypeName; - }); - }; - - return Vis; - }; -}); diff --git a/src/ui/public/Vis/VisType.js b/src/ui/public/Vis/VisType.js deleted file mode 100644 index 0c995bd108b14..0000000000000 --- a/src/ui/public/Vis/VisType.js +++ /dev/null @@ -1,21 +0,0 @@ -define(function (require) { - return function VisTypeFactory(Private) { - var VisTypeSchemas = Private(require('ui/Vis/Schemas')); - - function VisType(opts) { - opts = opts || {}; - - this.name = opts.name; - this.title = opts.title; - this.responseConverter = opts.responseConverter; - this.hierarchicalData = opts.hierarchicalData || false; - this.icon = opts.icon; - this.description = opts.description; - this.schemas = opts.schemas || new VisTypeSchemas(); - this.params = opts.params || {}; - this.requiresSearch = opts.requiresSearch == null ? true : opts.requiresSearch; // Default to true unless otherwise specified - } - - return VisType; - }; -}); diff --git a/src/ui/public/Vis/__tests__/_AggConfig.js b/src/ui/public/Vis/__tests__/_AggConfig.js deleted file mode 100644 index 579c120fc7e13..0000000000000 --- a/src/ui/public/Vis/__tests__/_AggConfig.js +++ /dev/null @@ -1,290 +0,0 @@ -describe('AggConfig', function () { - var sinon = require('auto-release-sinon'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - - var Vis; - var AggType; - var AggConfig; - var indexPattern; - var fieldFormat; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggType = Private(require('ui/agg_types/AggType')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - fieldFormat = Private(require('ui/registry/field_formats')); - })); - - describe('#toDsl', function () { - it('calls #write()', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - var stub = sinon.stub(aggConfig, 'write').returns({ params: {} }); - - aggConfig.toDsl(); - expect(stub.callCount).to.be(1); - }); - - it('uses the type name as the agg name', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - sinon.stub(aggConfig, 'write').returns({ params: {} }); - - var dsl = aggConfig.toDsl(); - expect(dsl).to.have.property('date_histogram'); - }); - - it('uses the params from #write() output as the agg params', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - var football = {}; - - sinon.stub(aggConfig, 'write').returns({ params: football }); - - var dsl = aggConfig.toDsl(); - expect(dsl.date_histogram).to.be(football); - }); - - it('includes subAggs from #write() output', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric' - }, - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var histoConfig = vis.aggs.byTypeName.date_histogram[0]; - var avgConfig = vis.aggs.byTypeName.avg[0]; - var football = {}; - - sinon.stub(histoConfig, 'write').returns({ params: {}, subAggs: [avgConfig] }); - sinon.stub(avgConfig, 'write').returns({ params: football }); - - var dsl = histoConfig.toDsl(); - - // didn't use .eql() because of variable key names, and final check is strict - expect(dsl).to.have.property('aggs'); - expect(dsl.aggs).to.have.property(avgConfig.id); - expect(dsl.aggs[avgConfig.id]).to.have.property('avg'); - expect(dsl.aggs[avgConfig.id].avg).to.be(football); - }); - }); - - describe('::ensureIds', function () { - it('accepts an array of objects and assigns ids to them', function () { - var objs = [ - {}, - {}, - {}, - {} - ]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '1'); - expect(objs[1]).to.have.property('id', '2'); - expect(objs[2]).to.have.property('id', '3'); - expect(objs[3]).to.have.property('id', '4'); - }); - - it('assigns ids relative to the other items in the list', function () { - var objs = [ - { id: '100' }, - {}, - ]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '100'); - expect(objs[1]).to.have.property('id', '101'); - }); - - it('assigns ids relative to the other items in the list', function () { - var objs = [ - { id: '100' }, - { id: '200' }, - { id: '500' }, - { id: '350' }, - {}, - ]; - AggConfig.ensureIds(objs); - expect(objs[0]).to.have.property('id', '100'); - expect(objs[1]).to.have.property('id', '200'); - expect(objs[2]).to.have.property('id', '500'); - expect(objs[3]).to.have.property('id', '350'); - expect(objs[4]).to.have.property('id', '501'); - }); - - it('uses ::nextId to get the starting value', function () { - sinon.stub(AggConfig, 'nextId').returns(534); - var objs = AggConfig.ensureIds([{}]); - expect(objs[0]).to.have.property('id', '534'); - }); - - it('only calls ::nextId once', function () { - var start = 420; - sinon.stub(AggConfig, 'nextId').returns(start); - var objs = AggConfig.ensureIds([{}, {}, {}, {}, {}, {}, {}]); - - expect(AggConfig.nextId).to.have.property('callCount', 1); - objs.forEach(function (obj, i) { - expect(obj).to.have.property('id', String(start + i)); - }); - }); - }); - - describe('::nextId', function () { - it('accepts a list of objects and picks the next id', function () { - var next = AggConfig.nextId([ {id: 100}, {id: 500} ]); - expect(next).to.be(501); - }); - - it('handles an empty list', function () { - var next = AggConfig.nextId([]); - expect(next).to.be(1); - }); - - it('fails when the list is not defined', function () { - expect(function () { - AggConfig.nextId(); - }).to.throwError(); - }); - }); - - describe('#toJSON', function () { - it('includes the aggs id, params, type and schema', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - expect(aggConfig.id).to.be('1'); - expect(aggConfig.params).to.be.an('object'); - expect(aggConfig.type).to.be.an(AggType).and.have.property('name', 'date_histogram'); - expect(aggConfig.schema).to.be.an('object').and.have.property('name', 'segment'); - - var state = aggConfig.toJSON(); - expect(state).to.have.property('id', '1'); - expect(state.params).to.be.an('object'); - expect(state).to.have.property('type', 'date_histogram'); - expect(state).to.have.property('schema', 'segment'); - }); - }); - - describe('#fieldFormatter', function () { - it('returns the fields format unless the agg type has a custom getFormat handler', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp' } - } - ] - }); - expect(vis.aggs[0].fieldFormatter()).to.be(vis.aggs[0].field().format.getConverterFor()); - - vis = new Vis(indexPattern, { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric', - params: { field: '@timestamp' } - } - ] - }); - expect(vis.aggs[0].fieldFormatter()).to.be(fieldFormat.getDefaultInstance('number').getConverterFor()); - }); - - it('returns the string format if the field does not have a format', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp' } - } - ] - }); - - var agg = vis.aggs[0]; - agg.params.field = { type: 'date', format: null }; - expect(agg.fieldFormatter()).to.be(fieldFormat.getDefaultInstance('string').getConverterFor()); - }); - - it('returns the string format if their is no field', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment', - params: { field: '@timestamp' } - } - ] - }); - - var agg = vis.aggs[0]; - delete agg.params.field; - expect(agg.fieldFormatter()).to.be(fieldFormat.getDefaultInstance('string').getConverterFor()); - }); - - it('returns the html converter if "html" is passed in', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'avg', - schema: 'metric', - params: { field: 'ssl' } - } - ] - }); - - var field = indexPattern.fields.byName.ssl; - expect(vis.aggs[0].fieldFormatter('html')).to.be(field.format.getConverterFor('html')); - }); - }); -}); diff --git a/src/ui/public/Vis/__tests__/_AggConfigResult.js b/src/ui/public/Vis/__tests__/_AggConfigResult.js deleted file mode 100644 index 9a48b95f433be..0000000000000 --- a/src/ui/public/Vis/__tests__/_AggConfigResult.js +++ /dev/null @@ -1,107 +0,0 @@ -describe('AggConfigResult', function () { - var _ = require('lodash'); - var AggConfigResult = require('ui/Vis/AggConfigResult'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - - var AggConfig; - var indexPattern; - var Vis; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - })); - - describe('initialization', function () { - it('should set the type to bucket for bucket based results', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ { type: 'terms', schema: 'segment', params: { field: '_type' } } ] - }); - var aggConfig = vis.aggs.byTypeName.terms[0]; - var results = new AggConfigResult(aggConfig, null, 10, 'apache'); - expect(results).to.have.property('aggConfig', aggConfig); - expect(results).to.have.property('$parent', null); - expect(results).to.have.property('type', 'bucket'); - expect(results).to.have.property('value', 10); - expect(results).to.have.property('key', 'apache'); - }); - - it('should set the type to metric for metric based results', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } } ] - }); - var aggConfig = vis.aggs.byTypeName.avg[0]; - var results = new AggConfigResult(aggConfig, null, 1024); - expect(results).to.have.property('aggConfig', aggConfig); - expect(results).to.have.property('$parent', null); - expect(results).to.have.property('type', 'metric'); - expect(results).to.have.property('value', 1024); - expect(results).to.have.property('key', undefined); - }); - }); - - - describe('hierarchical', function () { - describe('getPath()', function () { - - it('should return the parent and itself (in an array) for the path', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'segment', params: { field: '_type' } }, - { type: 'terms', schema: 'segment', params: { field: 'extension' } } - ] - }); - var parentAggConfig = vis.aggs.byTypeName.terms[0]; - var aggConfig = vis.aggs.byTypeName.terms[1]; - var parentResult = new AggConfigResult(parentAggConfig, null, 20, 'apache'); - var result = new AggConfigResult(aggConfig, parentResult, 15, 'php'); - var path = result.getPath(); - expect(path).to.be.an(Array); - expect(path).to.have.length(2); - expect(path[0]).to.be(parentResult); - expect(path[1]).to.be(result); - }); - - it('should return itself (in an array) for the path', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'segment', params: { field: 'extension' } } - ] - }); - var aggConfig = vis.aggs.byTypeName.terms[0]; - var result = new AggConfigResult(aggConfig, null, 15, 'php'); - var path = result.getPath(); - expect(path).to.be.an(Array); - expect(path).to.have.length(1); - expect(path[0]).to.be(result); - }); - - }); - - describe('createFilter', function () { - it('should return a filter object that represents the result', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'segment', params: { field: 'extension' } } - ] - }); - var aggConfig = vis.aggs.byTypeName.terms[0]; - var result = new AggConfigResult(aggConfig, null, 15, 'php'); - var filter = result.createFilter(); - expect(filter).to.have.property('query'); - expect(filter.query).to.have.property('match'); - expect(filter.query.match).to.have.property('extension'); - expect(filter.query.match.extension).to.have.property('query', 'php'); - expect(filter.query.match.extension).to.have.property('type', 'phrase'); - }); - }); - }); -}); diff --git a/src/ui/public/Vis/__tests__/_AggConfigs.js b/src/ui/public/Vis/__tests__/_AggConfigs.js deleted file mode 100644 index cd165a86c0682..0000000000000 --- a/src/ui/public/Vis/__tests__/_AggConfigs.js +++ /dev/null @@ -1,353 +0,0 @@ -describe('AggConfigs', function () { - var _ = require('lodash'); - var sinon = require('auto-release-sinon'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - - var Vis; - var IndexedArray; - var AggConfig; - var AggConfigs; - var SpiedAggConfig; - var indexPattern; - var Schemas; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - // replace the AggConfig module with a spy - var RealAggConfigPM = require('ui/Vis/AggConfig'); - AggConfig = Private(RealAggConfigPM); - var spy = sinon.spy(AggConfig); - Object.defineProperty(spy, 'aggTypes', { - get: function () { return AggConfig.aggTypes; }, - set: function (val) { AggConfig.aggTypes = val; } - }); - - Private.stub(RealAggConfigPM, spy); - - // load main deps - Vis = Private(require('ui/Vis')); - SpiedAggConfig = Private(require('ui/Vis/AggConfig')); - AggConfigs = Private(require('ui/Vis/AggConfigs')); - IndexedArray = require('ui/IndexedArray'); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - Schemas = Private(require('ui/Vis/Schemas')); - })); - - it('extends IndexedArray', function () { - var ac = new AggConfigs(); - expect(ac).to.be.a(IndexedArray); - }); - - describe('constructor', function () { - it('handles passing just a vis', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [] - }); - - var ac = new AggConfigs(vis); - expect(ac).to.have.length(1); - }); - - it('converts configStates into AggConfig objects if they are not already', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [] - }); - - var ac = new AggConfigs(vis, [ - { - type: 'date_histogram', - schema: 'segment' - }, - new AggConfig(vis, { - type: 'terms', - schema: 'split' - }) - ]); - - expect(ac).to.have.length(3); - expect(SpiedAggConfig).to.have.property('callCount', 3); - }); - - it('attemps to ensure that all states have an id', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [] - }); - - var states = [ - { - type: 'date_histogram', - schema: 'segment' - }, - { - type: 'terms', - schema: 'split' - } - ]; - - var spy = sinon.spy(SpiedAggConfig, 'ensureIds'); - var ac = new AggConfigs(vis, states); - expect(spy.callCount).to.be(1); - expect(spy.firstCall.args[0]).to.be(states); - }); - - describe('defaults', function () { - var vis; - beforeEach(function () { - vis = { - indexPattern: indexPattern, - type: { - schemas: new Schemas([ - { - group: 'metrics', - name: 'metric', - title: 'Simple', - min: 1, - max: 2, - defaults: [ - { schema: 'metric', type: 'count' }, - { schema: 'metric', type: 'avg' }, - { schema: 'metric', type: 'sum' } - ] - }, - { - group: 'buckets', - name: 'segment', - title: 'Example', - min: 0, - max: 1, - defaults: [ - { schema: 'segment', type: 'terms' }, - { schema: 'segment', type: 'filters' } - ] - } - ]) - } - }; - }); - - it('should only set the number of defaults defined by the max', function () { - var ac = new AggConfigs(vis); - expect(ac.bySchemaName.metric).to.have.length(2); - }); - - it('should set the defaults defined in the schema when none exist', function () { - var ac = new AggConfigs(vis); - expect(ac).to.have.length(3); - }); - - it('should NOT set the defaults defined in the schema when some exist', function () { - var ac = new AggConfigs(vis, [{ schema: 'segment', type: 'date_histogram' }]); - expect(ac).to.have.length(3); - expect(ac.bySchemaName.segment[0].type.name).to.equal('date_histogram'); - }); - }); - }); - - describe('#getRequestAggs', function () { - it('performs a stable sort, but moves metrics to the bottom', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'avg', schema: 'metric' }, - { type: 'terms', schema: 'split' }, - { type: 'histogram', schema: 'split' }, - { type: 'sum', schema: 'metric' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'filters', schema: 'split' }, - { type: 'percentiles', schema: 'metric' } - ] - }); - - var sorted = vis.aggs.getRequestAggs(); - var aggs = _.indexBy(vis.aggs, function (agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.histogram); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift()).to.be(aggs.filters); - expect(sorted.shift()).to.be(aggs.avg); - expect(sorted.shift()).to.be(aggs.sum); - expect(sorted.shift()).to.be(aggs.percentiles); - expect(sorted).to.have.length(0); - }); - }); - - describe('#getResponseAggs', function () { - it('returns all request aggs for basic aggs', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'split' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'count', schema: 'metric' } - ] - }); - - var sorted = vis.aggs.getResponseAggs(); - var aggs = _.indexBy(vis.aggs, function (agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift()).to.be(aggs.count); - expect(sorted).to.have.length(0); - }); - - it('expands aggs that have multiple responses', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'split' }, - { type: 'date_histogram', schema: 'segment' }, - { type: 'percentiles', schema: 'metric', params: { percents: [1, 2, 3]} } - ] - }); - - var sorted = vis.aggs.getResponseAggs(); - var aggs = _.indexBy(vis.aggs, function (agg) { - return agg.type.name; - }); - - expect(sorted.shift()).to.be(aggs.terms); - expect(sorted.shift()).to.be(aggs.date_histogram); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 1); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 2); - expect(sorted.shift().id).to.be(aggs.percentiles.id + '.' + 3); - expect(sorted).to.have.length(0); - }); - }); - - describe('#toDsl', function () { - it('uses the sorted aggs', function () { - var vis = new Vis(indexPattern, { type: 'histogram' }); - sinon.spy(vis.aggs, 'getRequestAggs'); - vis.aggs.toDsl(); - expect(vis.aggs.getRequestAggs).to.have.property('callCount', 1); - }); - - it('calls aggConfig#toDsl() on each aggConfig and compiles the nested output', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'date_histogram', schema: 'segment' }, - { type: 'filters', schema: 'split' } - ] - }); - - var aggInfos = vis.aggs.map(function (aggConfig) { - var football = {}; - - sinon.stub(aggConfig, 'toDsl', function () { - return football; - }); - - return { - id: aggConfig.id, - football: football - }; - }); - - (function recurse(lvl) { - var info = aggInfos.shift(); - - expect(lvl).to.have.property(info.id); - expect(lvl[info.id]).to.be(info.football); - - if (lvl[info.id].aggs) { - return recurse(lvl[info.id].aggs); - } - }(vis.aggs.toDsl())); - - expect(aggInfos).to.have.length(1); - }); - - it('skips aggs that don\'t have a dsl representation', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, - { type: 'count', schema: 'metric' } - ] - }); - - var dsl = vis.aggs.toDsl(); - var histo = vis.aggs.byTypeName.date_histogram[0]; - var count = vis.aggs.byTypeName.count[0]; - - expect(dsl).to.have.property(histo.id); - expect(dsl[histo.id]).to.be.an('object'); - expect(dsl[histo.id]).to.not.have.property('aggs'); - expect(dsl).to.not.have.property(count.id); - }); - - it('writes multiple metric aggregations at the same level', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { type: 'max', schema: 'metric', params: { field: 'bytes' } } - ] - }); - - var dsl = vis.aggs.toDsl(); - - var histo = vis.aggs.byTypeName.date_histogram[0]; - var metrics = vis.aggs.bySchemaGroup.metrics; - - expect(dsl).to.have.property(histo.id); - expect(dsl[histo.id]).to.be.an('object'); - expect(dsl[histo.id]).to.have.property('aggs'); - - metrics.forEach(function (metric) { - expect(dsl[histo.id].aggs).to.have.property(metric.id); - expect(dsl[histo.id].aggs[metric.id]).to.not.have.property('aggs'); - }); - }); - - it('writes multiple metric aggregations at every level if the vis is hierarchical', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { type: 'terms', schema: 'segment', params: { field: 'ip', orderBy: 1 } }, - { type: 'terms', schema: 'segment', params: { field: 'extension', orderBy: 1 } }, - { id: 1, type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'sum', schema: 'metric', params: { field: 'bytes' } }, - { type: 'min', schema: 'metric', params: { field: 'bytes' } }, - { type: 'max', schema: 'metric', params: { field: 'bytes' } } - ] - }); - vis.isHierarchical = _.constant(true); - - var topLevelDsl = vis.aggs.toDsl(); - var buckets = vis.aggs.bySchemaGroup.buckets; - var metrics = vis.aggs.bySchemaGroup.metrics; - - (function checkLevel(dsl) { - var bucket = buckets.shift(); - expect(dsl).to.have.property(bucket.id); - - expect(dsl[bucket.id]).to.be.an('object'); - expect(dsl[bucket.id]).to.have.property('aggs'); - - metrics.forEach(function (metric) { - expect(dsl[bucket.id].aggs).to.have.property(metric.id); - expect(dsl[bucket.id].aggs[metric.id]).to.not.have.property('aggs'); - }); - - if (buckets.length) { - checkLevel(dsl[bucket.id].aggs); - } - }(topLevelDsl)); - }); - }); -}); diff --git a/src/ui/public/Vis/__tests__/_Renderbot.js b/src/ui/public/Vis/__tests__/_Renderbot.js deleted file mode 100644 index a1bfd462fa0a1..0000000000000 --- a/src/ui/public/Vis/__tests__/_Renderbot.js +++ /dev/null @@ -1,37 +0,0 @@ -describe('renderbot', function () { - var Renderbot; - var expect = require('expect.js'); - var ngMock = require('ngMock'); - - function init() { - ngMock.module('kibana'); - - ngMock.inject(function (Private) { - Renderbot = Private(require('ui/Vis/Renderbot')); - }); - } - - describe('API', function () { - var vis; - var $el; - var renderbot; - - beforeEach(init); - beforeEach(function () { - vis = { hello: 'world' }; - $el = 'element'; - renderbot = new Renderbot(vis, $el); - }); - - it('should have expected methods', function () { - expect(renderbot).to.have.property('render'); - expect(renderbot).to.have.property('destroy'); - expect(renderbot).to.have.property('updateParams'); - }); - - it('should throw if not implemented', function () { - expect(renderbot.render).to.throwError(); - expect(renderbot.destroy).to.throwError(); - }); - }); -}); diff --git a/src/ui/public/Vis/__tests__/_Vis.js b/src/ui/public/Vis/__tests__/_Vis.js deleted file mode 100644 index f72e69e29af9d..0000000000000 --- a/src/ui/public/Vis/__tests__/_Vis.js +++ /dev/null @@ -1,107 +0,0 @@ -describe('Vis Class', function () { - - var _ = require('lodash'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - - var indexPattern; - var Vis; - var visTypes; - - var vis; - var stateFixture = { - type: 'pie', - aggs: [ - { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, - { type: 'terms', schema: 'segment', params: { field: 'machine.os' }}, - { type: 'terms', schema: 'segment', params: { field: 'geo.src' }} - ], - params: { isDonut: true }, - listeners: { click: _.noop } - }; - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - visTypes = Private(require('ui/registry/vis_types')); - })); - - beforeEach(function () { - vis = new Vis(indexPattern, stateFixture); - }); - - var verifyVis = function (vis) { - expect(vis).to.have.property('aggs'); - expect(vis.aggs).to.have.length(3); - - expect(vis).to.have.property('type'); - expect(vis.type).to.eql(visTypes.byName.pie); - - expect(vis).to.have.property('listeners'); - expect(vis.listeners).to.have.property('click'); - expect(vis.listeners.click).to.eql(_.noop); - - expect(vis).to.have.property('params'); - expect(vis.params).to.have.property('isDonut', true); - expect(vis).to.have.property('indexPattern', indexPattern); - }; - - describe('initialization', function () { - it('should set the state', function () { - verifyVis(vis); - }); - }); - - describe('getState()', function () { - it('should get a state that represents the... er... state', function () { - var state = vis.getState(); - expect(state).to.have.property('type', 'pie'); - - expect(state).to.have.property('params'); - expect(state.params).to.have.property('isDonut', true); - - expect(state).to.have.property('listeners'); - expect(state.listeners).to.have.property('click'); - expect(state.listeners.click).to.eql(_.noop); - - expect(state).to.have.property('aggs'); - expect(state.aggs).to.have.length(3); - }); - }); - - describe('clone()', function () { - it('should make clone of itself', function () { - var clone = vis.clone(); - verifyVis(clone); - }); - }); - - describe('setState()', function () { - it('should set the state to defualts', function () { - var vis = new Vis(indexPattern); - expect(vis).to.have.property('type'); - expect(vis.type).to.eql(visTypes.byName.histogram); - expect(vis).to.have.property('aggs'); - expect(vis.aggs).to.have.length(1); - expect(vis).to.have.property('listeners'); - expect(vis.listeners).to.eql({}); - expect(vis).to.have.property('params'); - expect(vis.params).to.have.property('addLegend', true); - expect(vis.params).to.have.property('addTooltip', true); - expect(vis.params).to.have.property('mode', 'stacked'); - expect(vis.params).to.have.property('shareYAxis', true); - }); - }); - - describe('isHierarchical()', function () { - it('should return true for hierarchical vis (like pie)', function () { - expect(vis.isHierarchical()).to.be(true); - }); - it('should return false for non-hierarchical vis (like histogram)', function () { - var vis = new Vis(indexPattern); - expect(vis.isHierarchical()).to.be(false); - }); - }); - -}); diff --git a/src/ui/public/Vis/__tests__/index.js b/src/ui/public/Vis/__tests__/index.js deleted file mode 100644 index 29e39f1ce3651..0000000000000 --- a/src/ui/public/Vis/__tests__/index.js +++ /dev/null @@ -1,6 +0,0 @@ -describe('Vis Component', function () { - require('./_AggConfig'); - require('./_AggConfigResult'); - require('./_AggConfigs'); - require('./_Vis'); -}); diff --git a/src/ui/public/__tests__/metadata.js b/src/ui/public/__tests__/metadata.js index 58e213c1761d3..62bb7813e7a0a 100644 --- a/src/ui/public/__tests__/metadata.js +++ b/src/ui/public/__tests__/metadata.js @@ -1,7 +1,7 @@ +import expect from 'expect.js'; +import metadata from 'ui/metadata'; describe('ui/metadata', () => { - const expect = require('expect.js'); - const metadata = require('ui/metadata'); it('is same data as window.__KBN__', () => { expect(metadata.version).to.equal(window.__KBN__.version); diff --git a/src/ui/public/agg_response/geo_json/__tests__/geo_json.js b/src/ui/public/agg_response/geo_json/__tests__/geo_json.js index f4ad5d637e614..94bb16a66cb98 100644 --- a/src/ui/public/agg_response/geo_json/__tests__/geo_json.js +++ b/src/ui/public/agg_response/geo_json/__tests__/geo_json.js @@ -1,23 +1,28 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import FixturesAggRespGeohashGridProvider from 'fixtures/agg_resp/geohash_grid'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json'; describe('GeoJson Agg Response Converter', function () { - var vis; - var tabify; - var convert; - var esResponse; - var aggs; + let vis; + let tabify; + let convert; + let esResponse; + let aggs; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - var Vis = Private(require('ui/Vis')); - var indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + let Vis = Private(VisProvider); + let indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - esResponse = Private(require('fixtures/agg_resp/geohash_grid')); - tabify = Private(require('ui/agg_response/tabify/tabify')); - convert = Private(require('ui/agg_response/geo_json/geo_json')); + esResponse = Private(FixturesAggRespGeohashGridProvider); + tabify = Private(AggResponseTabifyTabifyProvider); + convert = Private(AggResponseGeoJsonGeoJsonProvider); vis = new Vis(indexPattern, { type: 'tile_map', @@ -55,8 +60,8 @@ describe('GeoJson Agg Response Converter', function () { describe('with table ' + JSON.stringify(tableOpts), function () { it('outputs a chart', function () { - var table = makeTable(); - var chart = makeSingleChart(table); + let table = makeTable(); + let chart = makeSingleChart(table); expect(chart).to.only.have.keys( 'title', 'tooltipFormatter', @@ -73,9 +78,9 @@ describe('GeoJson Agg Response Converter', function () { }); it('outputs geohash points as features in a feature collection', function () { - var table = makeTable(); - var chart = makeSingleChart(table); - var geoJson = chart.geoJson; + let table = makeTable(); + let chart = makeSingleChart(table); + let geoJson = chart.geoJson; expect(geoJson.type).to.be('FeatureCollection'); expect(geoJson.features).to.be.an('array'); @@ -83,8 +88,8 @@ describe('GeoJson Agg Response Converter', function () { }); it('exports a bunch of properties about the geo hash grid', function () { - var geoJson = makeGeoJson(); - var props = geoJson.properties; + let geoJson = makeGeoJson(); + let props = geoJson.properties; // props expect(props).to.be.an('object'); @@ -103,10 +108,10 @@ describe('GeoJson Agg Response Converter', function () { describe('includes one feature per row in the table', function () { this.timeout(60000); - var table; - var chart; - var geoColI; - var metricColI; + let table; + let chart; + let geoColI; + let metricColI; before(function () { table = makeTable(); @@ -117,7 +122,7 @@ describe('GeoJson Agg Response Converter', function () { it('should be geoJson format', function () { table.rows.forEach(function (row, i) { - var feature = chart.geoJson.features[i]; + let feature = chart.geoJson.features[i]; expect(feature).to.have.property('geometry'); expect(feature.geometry).to.be.an('object'); expect(feature).to.have.property('properties'); @@ -127,7 +132,7 @@ describe('GeoJson Agg Response Converter', function () { it('should have valid geometry data', function () { table.rows.forEach(function (row, i) { - var geometry = chart.geoJson.features[i].geometry; + let geometry = chart.geoJson.features[i].geometry; expect(geometry.type).to.be('Point'); expect(geometry).to.have.property('coordinates'); expect(geometry.coordinates).to.be.an('array'); @@ -139,8 +144,8 @@ describe('GeoJson Agg Response Converter', function () { it('should have value properties data', function () { table.rows.forEach(function (row, i) { - var props = chart.geoJson.features[i].properties; - var keys = ['value', 'geohash', 'aggConfigResult', 'rectangle', 'center']; + let props = chart.geoJson.features[i].properties; + let keys = ['value', 'geohash', 'aggConfigResult', 'rectangle', 'center']; expect(props).to.be.an('object'); expect(props).to.only.have.keys(keys); expect(props.geohash).to.be.a('string'); @@ -150,15 +155,15 @@ describe('GeoJson Agg Response Converter', function () { it('should use latLng in properties and lngLat in geometry', function () { table.rows.forEach(function (row, i) { - var geometry = chart.geoJson.features[i].geometry; - var props = chart.geoJson.features[i].properties; + let geometry = chart.geoJson.features[i].geometry; + let props = chart.geoJson.features[i].properties; expect(props.center).to.eql(geometry.coordinates.slice(0).reverse()); }); }); it('should handle both AggConfig and non-AggConfig results', function () { table.rows.forEach(function (row, i) { - var props = chart.geoJson.features[i].properties; + let props = chart.geoJson.features[i].properties; if (tableOpts.asAggConfigResults) { expect(props.aggConfigResult).to.be(row[metricColI]); expect(props.value).to.be(row[metricColI].value); diff --git a/src/ui/public/agg_response/geo_json/_tooltip.html b/src/ui/public/agg_response/geo_json/_tooltip.html index 31540085da0b3..b0d07cb80e4b3 100644 --- a/src/ui/public/agg_response/geo_json/_tooltip.html +++ b/src/ui/public/agg_response/geo_json/_tooltip.html @@ -1,8 +1,8 @@ - - + +
    {{detail.label}}{{detail.value}}{{detail.label}}{{detail.value}}
    diff --git a/src/ui/public/agg_response/geo_json/_tooltip_formatter.js b/src/ui/public/agg_response/geo_json/_tooltip_formatter.js index 071fc482298be..f444d5b27d0d5 100644 --- a/src/ui/public/agg_response/geo_json/_tooltip_formatter.js +++ b/src/ui/public/agg_response/geo_json/_tooltip_formatter.js @@ -1,41 +1,40 @@ -define(function (require) { - return function TileMapTooltipFormatter($compile, $rootScope, Private) { - var $ = require('jquery'); - var _ = require('lodash'); +import $ from 'jquery'; +import _ from 'lodash'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +export default function TileMapTooltipFormatter($compile, $rootScope, Private) { - var fieldFormats = Private(require('ui/registry/field_formats')); - var $tooltipScope = $rootScope.$new(); - var $el = $('
    ').html(require('ui/agg_response/geo_json/_tooltip.html')); - $compile($el)($tooltipScope); + let fieldFormats = Private(RegistryFieldFormatsProvider); + let $tooltipScope = $rootScope.$new(); + let $el = $('
    ').html(require('ui/agg_response/geo_json/_tooltip.html')); + $compile($el)($tooltipScope); - return function tooltipFormatter(feature) { - if (!feature) return ''; + return function tooltipFormatter(feature) { + if (!feature) return ''; - var value = feature.properties.value; - var acr = feature.properties.aggConfigResult; - var vis = acr.aggConfig.vis; + let value = feature.properties.value; + let acr = feature.properties.aggConfigResult; + let vis = acr.aggConfig.vis; - var metricAgg = acr.aggConfig; - var geoFormat = _.get(vis.aggs, 'byTypeName.geohash_grid[0].format'); - if (!geoFormat) geoFormat = fieldFormats.getDefaultInstance('geo_point'); + let metricAgg = acr.aggConfig; + let geoFormat = _.get(vis.aggs, 'byTypeName.geohash_grid[0].format'); + if (!geoFormat) geoFormat = fieldFormats.getDefaultInstance('geo_point'); - $tooltipScope.details = [ - { - label: metricAgg.makeLabel(), - value: metricAgg.fieldFormatter()(value) - }, - { - label: 'Center', - value: geoFormat.convert({ - lat: feature.geometry.coordinates[1], - lon: feature.geometry.coordinates[0] - }) - } - ]; + $tooltipScope.details = [ + { + label: metricAgg.makeLabel(), + value: metricAgg.fieldFormatter()(value) + }, + { + label: 'Center', + value: geoFormat.convert({ + lat: feature.geometry.coordinates[1], + lon: feature.geometry.coordinates[0] + }) + } + ]; - $tooltipScope.$apply(); + $tooltipScope.$apply(); - return $el.html(); - }; + return $el.html(); }; -}); +}; diff --git a/src/ui/public/agg_response/geo_json/geo_json.js b/src/ui/public/agg_response/geo_json/geo_json.js index 4b883f13820a5..aac0fba222985 100644 --- a/src/ui/public/agg_response/geo_json/geo_json.js +++ b/src/ui/public/agg_response/geo_json/geo_json.js @@ -1,44 +1,43 @@ -define(function (require) { - return function TileMapConverterFn(Private, timefilter, $compile, $rootScope) { - var _ = require('lodash'); +import _ from 'lodash'; +import rowsToFeatures from 'ui/agg_response/geo_json/rows_to_features'; +import AggResponseGeoJsonTooltipFormatterProvider from 'ui/agg_response/geo_json/_tooltip_formatter'; +export default function TileMapConverterFn(Private, timefilter, $compile, $rootScope) { - var rowsToFeatures = require('ui/agg_response/geo_json/rowsToFeatures'); - var tooltipFormatter = Private(require('ui/agg_response/geo_json/_tooltip_formatter')); + let tooltipFormatter = Private(AggResponseGeoJsonTooltipFormatterProvider); - return function (vis, table) { + return function (vis, table) { - function columnIndex(schema) { - return _.findIndex(table.columns, function (col) { - return col.aggConfig.schema.name === schema; - }); - } + function columnIndex(schema) { + return _.findIndex(table.columns, function (col) { + return col.aggConfig.schema.name === schema; + }); + } - var geoI = columnIndex('segment'); - var metricI = columnIndex('metric'); - var geoAgg = _.get(table.columns, [geoI, 'aggConfig']); - var metricAgg = _.get(table.columns, [metricI, 'aggConfig']); + let geoI = columnIndex('segment'); + let metricI = columnIndex('metric'); + let geoAgg = _.get(table.columns, [geoI, 'aggConfig']); + let metricAgg = _.get(table.columns, [metricI, 'aggConfig']); - var features = rowsToFeatures(table, geoI, metricI); - var values = features.map(function (feature) { - return feature.properties.value; - }); + let features = rowsToFeatures(table, geoI, metricI); + let values = features.map(function (feature) { + return feature.properties.value; + }); - return { - title: table.title(), - valueFormatter: metricAgg && metricAgg.fieldFormatter(), - tooltipFormatter: tooltipFormatter, - geohashGridAgg: geoAgg, - geoJson: { - type: 'FeatureCollection', - features: features, - properties: { - min: _.min(values), - max: _.max(values), - zoom: _.get(geoAgg, 'params.mapZoom'), - center: _.get(geoAgg, 'params.mapCenter') - } + return { + title: table.title(), + valueFormatter: metricAgg && metricAgg.fieldFormatter(), + tooltipFormatter: tooltipFormatter, + geohashGridAgg: geoAgg, + geoJson: { + type: 'FeatureCollection', + features: features, + properties: { + min: _.min(values), + max: _.max(values), + zoom: geoAgg && geoAgg.vis.uiStateVal('mapZoom'), + center: geoAgg && geoAgg.vis.uiStateVal('mapCenter') } - }; + } }; }; -}); +}; diff --git a/src/ui/public/agg_response/geo_json/rowsToFeatures.js b/src/ui/public/agg_response/geo_json/rowsToFeatures.js deleted file mode 100644 index 5ce58a6ddbece..0000000000000 --- a/src/ui/public/agg_response/geo_json/rowsToFeatures.js +++ /dev/null @@ -1,55 +0,0 @@ -define(function (require) { - var decodeGeoHash = require('ui/utils/decode_geo_hash'); - var AggConfigResult = require('ui/Vis/AggConfigResult'); - var _ = require('lodash'); - - function getAcr(val) { - return val instanceof AggConfigResult ? val : null; - } - - function unwrap(val) { - return getAcr(val) ? val.value : val; - } - - function convertRowsToFeatures(table, geoI, metricI) { - return _.transform(table.rows, function (features, row) { - var geohash = unwrap(row[geoI]); - if (!geohash) return; - - // fetch latLn of northwest and southeast corners, and center point - var location = decodeGeoHash(geohash); - - var centerLatLng = [ - location.latitude[2], - location.longitude[2] - ]; - - // order is nw, ne, se, sw - var rectangle = [ - [location.latitude[0], location.longitude[0]], - [location.latitude[0], location.longitude[1]], - [location.latitude[1], location.longitude[1]], - [location.latitude[1], location.longitude[0]], - ]; - - // geoJson coords use LngLat, so we reverse the centerLatLng - // See here for details: http://geojson.org/geojson-spec.html#positions - features.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: centerLatLng.slice(0).reverse() - }, - properties: { - geohash: geohash, - value: unwrap(row[metricI]), - aggConfigResult: getAcr(row[metricI]), - center: centerLatLng, - rectangle: rectangle - } - }); - }, []); - } - - return convertRowsToFeatures; -}); diff --git a/src/ui/public/agg_response/geo_json/rows_to_features.js b/src/ui/public/agg_response/geo_json/rows_to_features.js new file mode 100644 index 0000000000000..634bfd26b5335 --- /dev/null +++ b/src/ui/public/agg_response/geo_json/rows_to_features.js @@ -0,0 +1,53 @@ +import decodeGeoHash from 'ui/utils/decode_geo_hash'; +import AggConfigResult from 'ui/vis/agg_config_result'; +import _ from 'lodash'; + +function getAcr(val) { + return val instanceof AggConfigResult ? val : null; +} + +function unwrap(val) { + return getAcr(val) ? val.value : val; +} + +function convertRowsToFeatures(table, geoI, metricI) { + return _.transform(table.rows, function (features, row) { + let geohash = unwrap(row[geoI]); + if (!geohash) return; + + // fetch latLn of northwest and southeast corners, and center point + let location = decodeGeoHash(geohash); + + let centerLatLng = [ + location.latitude[2], + location.longitude[2] + ]; + + // order is nw, ne, se, sw + let rectangle = [ + [location.latitude[0], location.longitude[0]], + [location.latitude[0], location.longitude[1]], + [location.latitude[1], location.longitude[1]], + [location.latitude[1], location.longitude[0]], + ]; + + // geoJson coords use LngLat, so we reverse the centerLatLng + // See here for details: http://geojson.org/geojson-spec.html#positions + features.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: centerLatLng.slice(0).reverse() + }, + properties: { + geohash: geohash, + value: unwrap(row[metricI]), + aggConfigResult: getAcr(row[metricI]), + center: centerLatLng, + rectangle: rectangle + } + }); + }, []); +} + +export default convertRowsToFeatures; diff --git a/src/ui/public/agg_response/hierarchical/__tests__/array_to_linked_list.js b/src/ui/public/agg_response/hierarchical/__tests__/array_to_linked_list.js index 176ae74e04f97..eeca8b79a6cd8 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/array_to_linked_list.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/array_to_linked_list.js @@ -1,10 +1,10 @@ -var arrayToLinkedList = require('ui/agg_response/hierarchical/_array_to_linked_list'); -var expect = require('expect.js'); +import arrayToLinkedList from 'ui/agg_response/hierarchical/_array_to_linked_list'; +import expect from 'expect.js'; describe('buildHierarchicalData()', function () { describe('arrayToLinkedList', function () { - var results; + let results; beforeEach(function () { results = arrayToLinkedList([ { id: 1 }, diff --git a/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js b/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js index efa1b9c003c41..a5e5555c81243 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/build_hierarchical_data.js @@ -1,15 +1,19 @@ -var _ = require('lodash'); -var fixtures = require('fixtures/fake_hierarchical_data'); -var sinon = require('auto-release-sinon'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); - -var Vis; -var Notifier; -var AggConfigs; -var indexPattern; -var buildHierarchicalData; +import _ from 'lodash'; +import fixtures from 'fixtures/fake_hierarchical_data'; +import sinon from 'auto-release-sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigsProvider from 'ui/vis/agg_configs'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggResponseHierarchicalBuildHierarchicalDataProvider from 'ui/agg_response/hierarchical/build_hierarchical_data'; + +let Vis; +let Notifier; +let AggConfigs; +let indexPattern; +let buildHierarchicalData; describe('buildHierarchicalData', function () { @@ -19,19 +23,19 @@ describe('buildHierarchicalData', function () { Notifier = $injector.get('Notifier'); sinon.stub(Notifier.prototype, 'error'); - Vis = Private(require('ui/Vis')); - AggConfigs = Private(require('ui/Vis/AggConfigs')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - buildHierarchicalData = Private(require('ui/agg_response/hierarchical/build_hierarchical_data')); + Vis = Private(VisProvider); + AggConfigs = Private(VisAggConfigsProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + buildHierarchicalData = Private(AggResponseHierarchicalBuildHierarchicalDataProvider); })); describe('metric only', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -44,7 +48,7 @@ describe('buildHierarchicalData', function () { }); it('should set the slices with one child to a consistent label', function () { - var checkLabel = 'Count'; + let checkLabel = 'Count'; expect(results).to.have.property('slices'); expect(results.slices).to.have.property('children'); expect(results.slices.children).to.have.length(1); @@ -63,8 +67,8 @@ describe('buildHierarchicalData', function () { describe('rows and columns', function () { it('should set the rows', function () { - var id = 1; - var vis = new Vis(indexPattern, { + let id = 1; + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -75,13 +79,13 @@ describe('buildHierarchicalData', function () { }); // We need to set the aggs to a known value. _.each(vis.aggs, function (agg) { agg.id = 'agg_' + id++; }); - var results = buildHierarchicalData(vis, fixtures.threeTermBuckets); + let results = buildHierarchicalData(vis, fixtures.threeTermBuckets); expect(results).to.have.property('rows'); }); it('should set the columns', function () { - var id = 1; - var vis = new Vis(indexPattern, { + let id = 1; + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -92,18 +96,18 @@ describe('buildHierarchicalData', function () { }); // We need to set the aggs to a known value. _.each(vis.aggs, function (agg) { agg.id = 'agg_' + id++; }); - var results = buildHierarchicalData(vis, fixtures.threeTermBuckets); + let results = buildHierarchicalData(vis, fixtures.threeTermBuckets); expect(results).to.have.property('columns'); }); }); describe('threeTermBuckets', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -141,11 +145,11 @@ describe('buildHierarchicalData', function () { }); describe('oneHistogramBucket', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -173,11 +177,11 @@ describe('buildHierarchicalData', function () { }); describe('oneRangeBucket', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -214,11 +218,11 @@ describe('buildHierarchicalData', function () { }); describe('oneFilterBucket', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -250,11 +254,11 @@ describe('buildHierarchicalData', function () { }); describe('oneFilterBucket that is a split', function () { - var vis; - var results; + let vis; + let results; beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -277,7 +281,7 @@ describe('buildHierarchicalData', function () { }); it('should set the hits attribute for the results', function () { - var errCall = Notifier.prototype.error.getCall(0); + let errCall = Notifier.prototype.error.getCall(0); expect(errCall).to.be.ok(); expect(errCall.args[0]).to.contain('not supported'); diff --git a/src/ui/public/agg_response/hierarchical/__tests__/collect_branch.js b/src/ui/public/agg_response/hierarchical/__tests__/collect_branch.js index 7de60241fc7e0..d153e7d4afc34 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/collect_branch.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/collect_branch.js @@ -1,10 +1,10 @@ -var _ = require('lodash'); -var collectBranch = require('ui/agg_response/hierarchical/_collect_branch'); -var expect = require('expect.js'); +import _ from 'lodash'; +import collectBranch from 'ui/agg_response/hierarchical/_collect_branch'; +import expect from 'expect.js'; describe('collectBranch()', function () { - var results; - var convert = function (name) { + let results; + let convert = function (name) { return 'converted:' + name; }; diff --git a/src/ui/public/agg_response/hierarchical/__tests__/create_raw_data.js b/src/ui/public/agg_response/hierarchical/__tests__/create_raw_data.js index ff1312c9eb51f..f42e4442c4fd1 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/create_raw_data.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/create_raw_data.js @@ -1,30 +1,33 @@ -var _ = require('lodash'); -var fixtures = require('fixtures/fake_hierarchical_data'); -var createRawData = require('ui/agg_response/hierarchical/_create_raw_data'); -var arrayToLinkedList = require('ui/agg_response/hierarchical/_array_to_linked_list'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import fixtures from 'fixtures/fake_hierarchical_data'; +import createRawData from 'ui/agg_response/hierarchical/_create_raw_data'; +import arrayToLinkedList from 'ui/agg_response/hierarchical/_array_to_linked_list'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigsProvider from 'ui/vis/agg_configs'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -var AggConfigs; -var Vis; -var indexPattern; +let AggConfigs; +let Vis; +let indexPattern; describe('buildHierarchicalData()', function () { describe('createRawData()', function () { - var vis; - var results; + let vis; + let results; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfigs = Private(require('ui/Vis/AggConfigs')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + Vis = Private(VisProvider); + AggConfigs = Private(VisAggConfigsProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); beforeEach(function () { - var id = 1; + let id = 1; vis = new Vis(indexPattern, { type: 'pie', aggs: [ @@ -34,7 +37,7 @@ describe('buildHierarchicalData()', function () { { type: 'terms', schema: 'segment', params: { field: 'geo.src' }} ] }); - var buckets = arrayToLinkedList(vis.aggs.bySchemaGroup.buckets); + let buckets = arrayToLinkedList(vis.aggs.bySchemaGroup.buckets); // We need to set the aggs to a known value. _.each(vis.aggs, function (agg) { agg.id = 'agg_' + id++; }); results = createRawData(vis, fixtures.threeTermBuckets); @@ -45,7 +48,7 @@ describe('buildHierarchicalData()', function () { expect(results.columns).to.have.length(6); _.each(results.columns, function (column) { expect(column).to.have.property('aggConfig'); - var agg = column.aggConfig; + let agg = column.aggConfig; expect(column).to.have.property('categoryName', agg.schema.name); expect(column).to.have.property('id', agg.id); expect(column).to.have.property('aggType', agg.type); diff --git a/src/ui/public/agg_response/hierarchical/__tests__/extract_buckets.js b/src/ui/public/agg_response/hierarchical/__tests__/extract_buckets.js index ebc0303cdd089..8d2a45c3a1f45 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/extract_buckets.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/extract_buckets.js @@ -1,20 +1,20 @@ -var extractBuckets = require('ui/agg_response/hierarchical/_extract_buckets'); -var expect = require('expect.js'); +import extractBuckets from 'ui/agg_response/hierarchical/_extract_buckets'; +import expect from 'expect.js'; describe('buildHierarchicalData()', function () { describe('extractBuckets()', function () { it('should normalize a bucket object into an array', function () { - var bucket = { + let bucket = { buckets: { foo: { doc_count: 1 }, bar: { doc_count: 2 } } }; - var buckets = extractBuckets(bucket); + let buckets = extractBuckets(bucket); expect(buckets).to.be.an(Array); expect(buckets).to.have.length(2); expect(buckets[0]).to.have.property('key', 'foo'); @@ -24,19 +24,19 @@ describe('buildHierarchicalData()', function () { }); it('should return an empty array for undefined buckets', function () { - var buckets = extractBuckets(); + let buckets = extractBuckets(); expect(buckets).to.be.an(Array); expect(buckets).to.have.length(0); }); it('should return the bucket array', function () { - var bucket = { + let bucket = { buckets: [ { key: 'foo', doc_count: 1 }, { key: 'bar', doc_count: 2 } ] }; - var buckets = extractBuckets(bucket); + let buckets = extractBuckets(bucket); expect(buckets).to.be.an(Array); expect(buckets).to.be(bucket.buckets); }); diff --git a/src/ui/public/agg_response/hierarchical/__tests__/transform_aggregation.js b/src/ui/public/agg_response/hierarchical/__tests__/transform_aggregation.js index 378df0c65d4cb..97adf8ea4073b 100644 --- a/src/ui/public/agg_response/hierarchical/__tests__/transform_aggregation.js +++ b/src/ui/public/agg_response/hierarchical/__tests__/transform_aggregation.js @@ -1,15 +1,16 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseHierarchicalTransformAggregationProvider from 'ui/agg_response/hierarchical/_transform_aggregation'; describe('buildHierarchicalData()', function () { describe('transformAggregation()', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var transform; - var fixture; + let transform; + let fixture; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - transform = Private(require('ui/agg_response/hierarchical/_transform_aggregation')); + transform = Private(AggResponseHierarchicalTransformAggregationProvider); })); beforeEach(function () { @@ -41,17 +42,17 @@ describe('buildHierarchicalData()', function () { }); it('relies on metricAgg#getValue() for the size of the children', function () { - var aggData = { + let aggData = { buckets: [ { key: 'foo' }, { key: 'bar' } ] }; - var football = {}; + let football = {}; fixture.metric.getValue = _.constant(football); - var children = transform(fixture.agg, fixture.metric, aggData); + let children = transform(fixture.agg, fixture.metric, aggData); expect(children).to.be.an(Array); expect(children).to.have.length(2); expect(children[0]).to.have.property('size', football); @@ -59,7 +60,7 @@ describe('buildHierarchicalData()', function () { }); it('should create two levels of metrics', function () { - var children = transform(fixture.agg, fixture.metric, fixture.aggData); + let children = transform(fixture.agg, fixture.metric, fixture.aggData); fixture.metric.getValue = function (b) { return b.doc_count; }; expect(children).to.be.an(Array); diff --git a/src/ui/public/agg_response/hierarchical/_array_to_linked_list.js b/src/ui/public/agg_response/hierarchical/_array_to_linked_list.js index fb8b236cd0c0c..9a2d58f4ecb4d 100644 --- a/src/ui/public/agg_response/hierarchical/_array_to_linked_list.js +++ b/src/ui/public/agg_response/hierarchical/_array_to_linked_list.js @@ -1,14 +1,12 @@ -define(function (require) { - var _ = require('lodash'); - return function (buckets) { - var previous; - _.each(buckets, function (bucket) { - if (previous) { - bucket._previous = previous; - previous._next = bucket; - } - previous = bucket; - }); - return buckets; - }; -}); +import _ from 'lodash'; +export default function (buckets) { + let previous; + _.each(buckets, function (bucket) { + if (previous) { + bucket._previous = previous; + previous._next = bucket; + } + previous = bucket; + }); + return buckets; +}; diff --git a/src/ui/public/agg_response/hierarchical/_build_split.js b/src/ui/public/agg_response/hierarchical/_build_split.js index 7c6bacc9c5f7f..f4eb0fdf764ab 100644 --- a/src/ui/public/agg_response/hierarchical/_build_split.js +++ b/src/ui/public/agg_response/hierarchical/_build_split.js @@ -1,17 +1,16 @@ -define(function (require) { - return function biuldSplitProvider(Private) { - var transformer = Private(require('ui/agg_response/hierarchical/_transform_aggregation')); - var collectKeys = require('ui/agg_response/hierarchical/_collect_keys'); - return function (agg, metric, aggData) { - // Ceate the split structure - var split = { label: '', slices: { children: [] } }; +import collectKeys from 'ui/agg_response/hierarchical/_collect_keys'; +import AggResponseHierarchicalTransformAggregationProvider from 'ui/agg_response/hierarchical/_transform_aggregation'; +export default function biuldSplitProvider(Private) { + let transformer = Private(AggResponseHierarchicalTransformAggregationProvider); + return function (agg, metric, aggData) { + // Ceate the split structure + let split = { label: '', slices: { children: [] } }; - // Transform the aggData into splits - split.slices.children = transformer(agg, metric, aggData); + // Transform the aggData into splits + split.slices.children = transformer(agg, metric, aggData); - // Collect all the keys - split.names = collectKeys(split.slices.children); - return split; - }; + // Collect all the keys + split.names = collectKeys(split.slices.children); + return split; }; -}); +}; diff --git a/src/ui/public/agg_response/hierarchical/_collect_branch.js b/src/ui/public/agg_response/hierarchical/_collect_branch.js index 97370399f0e4e..5ad6518388278 100644 --- a/src/ui/public/agg_response/hierarchical/_collect_branch.js +++ b/src/ui/public/agg_response/hierarchical/_collect_branch.js @@ -3,13 +3,13 @@ define(function () { // walk up the branch for each parent function walk(item, memo) { // record the the depth - var depth = item.depth - 1; + let depth = item.depth - 1; // Using the aggConfig determine what the field name is. If the aggConfig // doesn't exist (which means it's an _all agg) then use the level for // the field name - var col = item.aggConfig; - var field = (col && col.params && col.params.field && col.params.field.displayName) + let col = item.aggConfig; + let field = (col && col.params && col.params.field && col.params.field.displayName) || (col && col.label) || ('level ' + item.depth); diff --git a/src/ui/public/agg_response/hierarchical/_collect_keys.js b/src/ui/public/agg_response/hierarchical/_collect_keys.js index 460e6c0091e2e..1b9a542462c75 100644 --- a/src/ui/public/agg_response/hierarchical/_collect_keys.js +++ b/src/ui/public/agg_response/hierarchical/_collect_keys.js @@ -1,12 +1,10 @@ -define(function (require) { - var _ = require('lodash'); - return function collectKeys(children) { - var nextChildren = _.pluck(children, 'children'); - var keys = _.pluck(children, 'name'); - return _(nextChildren) - .map(collectKeys) - .flattenDeep() - .union(keys) - .value(); - }; -}); +import _ from 'lodash'; +export default function collectKeys(children) { + let nextChildren = _.pluck(children, 'children'); + let keys = _.pluck(children, 'name'); + return _(nextChildren) + .map(collectKeys) + .flattenDeep() + .union(keys) + .value(); +}; diff --git a/src/ui/public/agg_response/hierarchical/_create_raw_data.js b/src/ui/public/agg_response/hierarchical/_create_raw_data.js index 9f4346a9fca57..93b9c1b519bfe 100644 --- a/src/ui/public/agg_response/hierarchical/_create_raw_data.js +++ b/src/ui/public/agg_response/hierarchical/_create_raw_data.js @@ -1,94 +1,92 @@ -define(function (require) { - var _ = require('lodash'); - var extractBuckets = require('ui/agg_response/hierarchical/_extract_buckets'); - return function (vis, resp) { +import _ from 'lodash'; +import extractBuckets from 'ui/agg_response/hierarchical/_extract_buckets'; +export default function (vis, resp) { - // Create the initial results structure - var results = { rows: [] }; + // Create the initial results structure + let results = { rows: [] }; - // Create a reference to the buckets and metrics - var metrics = vis.aggs.bySchemaGroup.metrics; - var buckets = vis.aggs.bySchemaGroup.buckets; - var aggs = []; + // Create a reference to the buckets and metrics + let metrics = vis.aggs.bySchemaGroup.metrics; + let buckets = vis.aggs.bySchemaGroup.buckets; + let aggs = []; - if (buckets) { - _.each(buckets, function (bucket) { - aggs.push(bucket); - aggs.push(metrics); - }); - } else { + if (buckets) { + _.each(buckets, function (bucket) { + aggs.push(bucket); aggs.push(metrics); - } + }); + } else { + aggs.push(metrics); + } - // Create the columns - results.columns = _(aggs) - .flattenDeep() - .map(function (agg) { - return { - categoryName: agg.schema.name, - id: agg.id, - aggConfig: agg, - aggType: agg.type, - field: agg.params.field, - label: agg.type.makeLabel(agg) - }; - }) - .value(); + // Create the columns + results.columns = _(aggs) + .flattenDeep() + .map(function (agg) { + return { + categoryName: agg.schema.name, + id: agg.id, + aggConfig: agg, + aggType: agg.type, + field: agg.params.field, + label: agg.type.makeLabel(agg) + }; + }) + .value(); - // if there are no buckets then we need to just set the value and return - if (!buckets) { - var value = resp.aggregations - && resp.aggregations[metrics[0].id] - && resp.aggregations[metrics[0].id].value - || resp.hits.total; - results.rows.push([value]); - return results; - } - - /** - * Walk the buckets and create records for each leaf - * @param {aggConfig} agg The aggConfig for the current level - * @param {object} data The aggergation object - * @param {array} [record] The record that will eventually get pushed to the rows - * @returns {void} - */ - function walkBuckets(agg, data, record) { - if (!_.isArray(record)) { - record = []; - } + // if there are no buckets then we need to just set the value and return + if (!buckets) { + let value = resp.aggregations + && resp.aggregations[metrics[0].id] + && resp.aggregations[metrics[0].id].value + || resp.hits.total; + results.rows.push([value]); + return results; + } - // iterate through all the buckets - _.each(extractBuckets(data[agg.id], agg), function (bucket) { + /** + * Walk the buckets and create records for each leaf + * @param {aggConfig} agg The aggConfig for the current level + * @param {object} data The aggergation object + * @param {array} [record] The record that will eventually get pushed to the rows + * @returns {void} + */ + function walkBuckets(agg, data, record) { + if (!_.isArray(record)) { + record = []; + } - var _record = _.flattenDeep([record, bucket.key]); - _.each(metrics, function (metric) { - var value = bucket.doc_count; - if (bucket[metric.id] && !_.isUndefined(bucket[metric.id].value)) { - value = bucket[metric.id].value; - } - _record.push(value); - }); + // iterate through all the buckets + _.each(extractBuckets(data[agg.id], agg), function (bucket) { - // If there is another agg to call we need to check to see if it has - // buckets. If it does then we need to keep on walking the tree. - // This is where the recursion happens. - if (agg._next) { - var nextBucket = bucket[agg._next.id]; - if (nextBucket && nextBucket.buckets) { - walkBuckets(agg._next, bucket, _record); - } - } - // if there are no more aggs to walk then push the record to the rows. - else { - results.rows.push(_record); + let _record = _.flattenDeep([record, bucket.key]); + _.each(metrics, function (metric) { + let value = bucket.doc_count; + if (bucket[metric.id] && !_.isUndefined(bucket[metric.id].value)) { + value = bucket[metric.id].value; } + _record.push(value); }); - } - // Start walking the buckets at the beginning of the aggregations object. - walkBuckets(buckets[0], resp.aggregations); + // If there is another agg to call we need to check to see if it has + // buckets. If it does then we need to keep on walking the tree. + // This is where the recursion happens. + if (agg._next) { + let nextBucket = bucket[agg._next.id]; + if (nextBucket && nextBucket.buckets) { + walkBuckets(agg._next, bucket, _record); + } + } + // if there are no more aggs to walk then push the record to the rows. + else { + results.rows.push(_record); + } + }); + } - return results; - }; -}); + // Start walking the buckets at the beginning of the aggregations object. + walkBuckets(buckets[0], resp.aggregations); + + return results; +}; diff --git a/src/ui/public/agg_response/hierarchical/_extract_buckets.js b/src/ui/public/agg_response/hierarchical/_extract_buckets.js index e1866ea5d0062..e80f408664535 100644 --- a/src/ui/public/agg_response/hierarchical/_extract_buckets.js +++ b/src/ui/public/agg_response/hierarchical/_extract_buckets.js @@ -1,15 +1,13 @@ -define(function (require) { - var _ = require('lodash'); - return function (bucket, agg) { - if (bucket && _.isPlainObject(bucket.buckets)) { - return _.map(bucket.buckets, function (value, key) { - var item = _.cloneDeep(value); - item.key = agg ? agg.getKey(value, key) : key; - return item; - }); +import _ from 'lodash'; +export default function (bucket, agg) { + if (bucket && _.isPlainObject(bucket.buckets)) { + return _.map(bucket.buckets, function (value, key) { + let item = _.cloneDeep(value); + item.key = agg ? agg.getKey(value, key) : key; + return item; + }); - } else { - return bucket && bucket.buckets || []; - } - }; -}); + } else { + return bucket && bucket.buckets || []; + } +}; diff --git a/src/ui/public/agg_response/hierarchical/_hierarchical_tooltip_formatter.js b/src/ui/public/agg_response/hierarchical/_hierarchical_tooltip_formatter.js index bfae524ebf840..f4acab3927a57 100644 --- a/src/ui/public/agg_response/hierarchical/_hierarchical_tooltip_formatter.js +++ b/src/ui/public/agg_response/hierarchical/_hierarchical_tooltip_formatter.js @@ -1,46 +1,44 @@ -define(function (require) { - return function HierarchicalTooltipFormaterProvider($rootScope, $compile, $sce) { - var _ = require('lodash'); - var $ = require('jquery'); - var $tooltip = $(require('ui/agg_response/hierarchical/_tooltip.html')); - var collectBranch = require('ui/agg_response/hierarchical/_collect_branch'); - var $tooltipScope = $rootScope.$new(); - var numeral = require('numeral'); +import _ from 'lodash'; +import $ from 'jquery'; +import collectBranch from 'ui/agg_response/hierarchical/_collect_branch'; +import numeral from 'numeral'; +export default function HierarchicalTooltipFormaterProvider($rootScope, $compile, $sce) { + let $tooltip = $(require('ui/agg_response/hierarchical/_tooltip.html')); + let $tooltipScope = $rootScope.$new(); - $compile($tooltip)($tooltipScope); + $compile($tooltip)($tooltipScope); - return function (columns) { - return function (event) { - var datum = event.datum; + return function (columns) { + return function (event) { + let datum = event.datum; - // Collect the current leaf and parents into an array of values - $tooltipScope.rows = collectBranch(datum); + // Collect the current leaf and parents into an array of values + $tooltipScope.rows = collectBranch(datum); - var metricCol = $tooltipScope.metricCol = _.find(columns, { categoryName: 'metric' }); + let metricCol = $tooltipScope.metricCol = _.find(columns, { categoryName: 'metric' }); - // Map those values to what the tooltipSource.rows format. - _.forEachRight($tooltipScope.rows, function (row, i, rows) { - row.spacer = $sce.trustAsHtml(_.repeat(' ', row.depth)); + // Map those values to what the tooltipSource.rows format. + _.forEachRight($tooltipScope.rows, function (row, i, rows) { + row.spacer = $sce.trustAsHtml(_.repeat(' ', row.depth)); - var percent; - if (row.item.percentOfGroup != null) { - percent = row.item.percentOfGroup; - } + let percent; + if (row.item.percentOfGroup != null) { + percent = row.item.percentOfGroup; + } - row.metric = metricCol.aggConfig.fieldFormatter()(row.metric); + row.metric = metricCol.aggConfig.fieldFormatter()(row.metric); - if (percent != null) { - row.metric += ' (' + numeral(percent).format('0.[00]%') + ')'; - } + if (percent != null) { + row.metric += ' (' + numeral(percent).format('0.[00]%') + ')'; + } - return row; - }); - - $tooltipScope.$apply(); - return $tooltip[0].outerHTML; - }; + return row; + }); + $tooltipScope.$apply(); + return $tooltip[0].outerHTML; }; }; -}); + +}; diff --git a/src/ui/public/agg_response/hierarchical/_tooltip.html b/src/ui/public/agg_response/hierarchical/_tooltip.html index 44527343e728b..e714441492663 100644 --- a/src/ui/public/agg_response/hierarchical/_tooltip.html +++ b/src/ui/public/agg_response/hierarchical/_tooltip.html @@ -1,16 +1,16 @@ - + - + -
    field value {{metricCol.label}}
    {{row.field}} {{row.bucket}} {{row.metric}}
    \ No newline at end of file + diff --git a/src/ui/public/agg_response/hierarchical/_transform_aggregation.js b/src/ui/public/agg_response/hierarchical/_transform_aggregation.js index 0a3342ce8d23f..a176f2d3e0154 100644 --- a/src/ui/public/agg_response/hierarchical/_transform_aggregation.js +++ b/src/ui/public/agg_response/hierarchical/_transform_aggregation.js @@ -1,41 +1,39 @@ -define(function (require) { - var _ = require('lodash'); - var extractBuckets = require('ui/agg_response/hierarchical/_extract_buckets'); - return function transformAggregationProvider(Private) { - var AggConfigResult = require('ui/Vis/AggConfigResult'); - return function transformAggregation(agg, metric, aggData, parent) { - return _.map(extractBuckets(aggData, agg), function (bucket) { - var aggConfigResult = new AggConfigResult( - agg, - parent && parent.aggConfigResult, - metric.getValue(bucket), - agg.getKey(bucket) - ); +import _ from 'lodash'; +import extractBuckets from 'ui/agg_response/hierarchical/_extract_buckets'; +import AggConfigResult from 'ui/vis/agg_config_result'; +export default function transformAggregationProvider(Private) { + return function transformAggregation(agg, metric, aggData, parent) { + return _.map(extractBuckets(aggData, agg), function (bucket) { + let aggConfigResult = new AggConfigResult( + agg, + parent && parent.aggConfigResult, + metric.getValue(bucket), + agg.getKey(bucket) + ); - var branch = { - name: agg.fieldFormatter()(bucket.key), - size: aggConfigResult.value, - aggConfig: agg, - aggConfigResult: aggConfigResult - }; + let branch = { + name: agg.fieldFormatter()(bucket.key), + size: aggConfigResult.value, + aggConfig: agg, + aggConfigResult: aggConfigResult + }; - // if the parent is defined then we need to set the parent of the branch - // this will be used later for filters for waking up the parent path. - if (parent) { - branch.parent = parent; - } + // if the parent is defined then we need to set the parent of the branch + // this will be used later for filters for waking up the parent path. + if (parent) { + branch.parent = parent; + } - // If the next bucket exists and it has children the we need to - // transform it as well. This is where the recursion happens. - if (agg._next) { - var nextBucket = bucket[agg._next.id]; - if (nextBucket && nextBucket.buckets) { - branch.children = transformAggregation(agg._next, metric, nextBucket, branch); - } + // If the next bucket exists and it has children the we need to + // transform it as well. This is where the recursion happens. + if (agg._next) { + let nextBucket = bucket[agg._next.id]; + if (nextBucket && nextBucket.buckets) { + branch.children = transformAggregation(agg._next, metric, nextBucket, branch); } + } - return branch; - }); - }; + return branch; + }); }; -}); +}; diff --git a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js index 31ca1ca4b0b69..32c782bf6622e 100644 --- a/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js +++ b/src/ui/public/agg_response/hierarchical/build_hierarchical_data.js @@ -1,97 +1,97 @@ -define(function (require) { - return function buildHierarchicalDataProvider(Private, Notifier) { - var _ = require('lodash'); - var buildSplit = Private(require('ui/agg_response/hierarchical/_build_split')); - var extractBuckets = require('ui/agg_response/hierarchical/_extract_buckets'); - var createRawData = require('ui/agg_response/hierarchical/_create_raw_data'); - var arrayToLinkedList = require('ui/agg_response/hierarchical/_array_to_linked_list'); - var tooltipFormatter = Private(require('ui/agg_response/hierarchical/_hierarchical_tooltip_formatter')); - - var AggConfigResult = require('ui/Vis/AggConfigResult'); - - var notify = new Notifier({ - location: 'Pie chart response converter' - }); - - return function (vis, resp) { - // Create a refrenece to the buckets - var buckets = vis.aggs.bySchemaGroup.buckets; - - - // Find the metric so it's easier to reference. - // TODO: Change this to support multiple metrics. - var metric = vis.aggs.bySchemaGroup.metrics[0]; - - // Link each agg to the next agg. This will be - // to identify the next bucket aggregation - buckets = arrayToLinkedList(buckets); - - // Create the raw data to be used in the spy panel - var raw = createRawData(vis, resp); - - // If buckets is falsy then we should just return the aggs - if (!buckets) { - var label = 'Count'; - var value = resp.aggregations - && resp.aggregations[metric.id] - && resp.aggregations[metric.id].value - || resp.hits.total; - return { - hits: resp.hits.total, - raw: raw, - names: [label], - tooltipFormatter: tooltipFormatter(raw.columns), - slices: { - children: [ - { name: label, size: value } - ] - } - }; - } - - var firstAgg = buckets[0]; - var aggData = resp.aggregations[firstAgg.id]; - - if (!firstAgg._next && firstAgg.schema.name === 'split') { - notify.error('Splitting charts without splitting slices is not supported. Pretending that we are just splitting slices.'); - } - - // start with splitting slices - if (!firstAgg._next || firstAgg.schema.name === 'segment') { - var split = buildSplit(firstAgg, metric, aggData); - split.hits = resp.hits.total; - split.raw = raw; - split.tooltipFormatter = tooltipFormatter(raw.columns); - return split; - } - - // map the split aggregations into rows. - var rows = _.map(extractBuckets(aggData, firstAgg), function (bucket) { - var agg = firstAgg._next; - var split = buildSplit(agg, metric, bucket[agg.id]); - // Since splits display labels we need to set it. - split.label = firstAgg.fieldFormatter()(agg.getKey(bucket)); - - var displayName = firstAgg.fieldDisplayName(); - if (!_.isEmpty(displayName)) split.label += ': ' + displayName; - - split.tooltipFormatter = tooltipFormatter(raw.columns); - var aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket)); - split.split = { aggConfig: firstAgg, aggConfigResult: aggConfigResult, key: bucket.key }; - _.each(split.slices.children, function (child) { - child.aggConfigResult.$parent = aggConfigResult; - }); - return split; +import _ from 'lodash'; +import extractBuckets from 'ui/agg_response/hierarchical/_extract_buckets'; +import createRawData from 'ui/agg_response/hierarchical/_create_raw_data'; +import arrayToLinkedList from 'ui/agg_response/hierarchical/_array_to_linked_list'; +import AggConfigResult from 'ui/vis/agg_config_result'; +import AggResponseHierarchicalBuildSplitProvider from 'ui/agg_response/hierarchical/_build_split'; +import AggResponseHierarchicalHierarchicalTooltipFormatterProvider from 'ui/agg_response/hierarchical/_hierarchical_tooltip_formatter'; +export default function buildHierarchicalDataProvider(Private, Notifier) { + let buildSplit = Private(AggResponseHierarchicalBuildSplitProvider); + let tooltipFormatter = Private(AggResponseHierarchicalHierarchicalTooltipFormatterProvider); + + + let notify = new Notifier({ + location: 'Pie chart response converter' + }); + + return function (vis, resp) { + // Create a refrenece to the buckets + let buckets = vis.aggs.bySchemaGroup.buckets; + + + // Find the metric so it's easier to reference. + // TODO: Change this to support multiple metrics. + let metric = vis.aggs.bySchemaGroup.metrics[0]; + + // Link each agg to the next agg. This will be + // to identify the next bucket aggregation + buckets = arrayToLinkedList(buckets); + + // Create the raw data to be used in the spy panel + let raw = createRawData(vis, resp); + + // If buckets is falsy then we should just return the aggs + if (!buckets) { + let label = 'Count'; + let value = resp.aggregations + && resp.aggregations[metric.id] + && resp.aggregations[metric.id].value + || resp.hits.total; + return { + hits: resp.hits.total, + raw: raw, + names: [label], + tooltipFormatter: tooltipFormatter(raw.columns), + slices: { + children: [ + { name: label, size: value } + ] + } + }; + } + + let firstAgg = buckets[0]; + let aggData = resp.aggregations[firstAgg.id]; + + if (!firstAgg._next && firstAgg.schema.name === 'split') { + notify.error('Splitting charts without splitting slices is not supported. Pretending that we are just splitting slices.'); + } + + // start with splitting slices + if (!firstAgg._next || firstAgg.schema.name === 'segment') { + let split = buildSplit(firstAgg, metric, aggData); + split.hits = resp.hits.total; + split.raw = raw; + split.tooltipFormatter = tooltipFormatter(raw.columns); + return split; + } + + // map the split aggregations into rows. + let rows = _.map(extractBuckets(aggData, firstAgg), function (bucket) { + let agg = firstAgg._next; + let split = buildSplit(agg, metric, bucket[agg.id]); + // Since splits display labels we need to set it. + split.label = firstAgg.fieldFormatter()(agg.getKey(bucket)); + + let displayName = firstAgg.fieldDisplayName(); + if (!_.isEmpty(displayName)) split.label += ': ' + displayName; + + split.tooltipFormatter = tooltipFormatter(raw.columns); + let aggConfigResult = new AggConfigResult(firstAgg, null, null, firstAgg.getKey(bucket)); + split.split = { aggConfig: firstAgg, aggConfigResult: aggConfigResult, key: bucket.key }; + _.each(split.slices.children, function (child) { + child.aggConfigResult.$parent = aggConfigResult; }); + return split; + }); - var result = { hits: resp.hits.total, raw: raw }; - if (firstAgg.params.row) { - result.rows = rows; - } else { - result.columns = rows; - } + let result = { hits: resp.hits.total, raw: raw }; + if (firstAgg.params.row) { + result.rows = rows; + } else { + result.columns = rows; + } - return result; - }; + return result; }; -}); +}; diff --git a/src/ui/public/agg_response/index.js b/src/ui/public/agg_response/index.js index 1d9aeecc10124..ae098a8cb8536 100644 --- a/src/ui/public/agg_response/index.js +++ b/src/ui/public/agg_response/index.js @@ -1,10 +1,13 @@ -define(function (require) { - return function NormalizeChartDataFactory(Private) { - return { - hierarchical: Private(require('ui/agg_response/hierarchical/build_hierarchical_data')), - pointSeries: Private(require('ui/agg_response/point_series/point_series')), - tabify: Private(require('ui/agg_response/tabify/tabify')), - geoJson: Private(require('ui/agg_response/geo_json/geo_json')) - }; +import AggResponseHierarchicalBuildHierarchicalDataProvider from 'ui/agg_response/hierarchical/build_hierarchical_data'; +import AggResponsePointSeriesPointSeriesProvider from 'ui/agg_response/point_series/point_series'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json'; + +export default function NormalizeChartDataFactory(Private) { + return { + hierarchical: Private(AggResponseHierarchicalBuildHierarchicalDataProvider), + pointSeries: Private(AggResponsePointSeriesPointSeriesProvider), + tabify: Private(AggResponseTabifyTabifyProvider), + geoJson: Private(AggResponseGeoJsonGeoJsonProvider) }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js index 0e7042d8a2e3a..4e9c563f78dbf 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/__tests__/_add_to_siri.js @@ -1,17 +1,18 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesAddToSiriProvider from 'ui/agg_response/point_series/_add_to_siri'; describe('addToSiri', function () { - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var addToSiri; + let addToSiri; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - addToSiri = Private(require('ui/agg_response/point_series/_add_to_siri')); + addToSiri = Private(AggResponsePointSeriesAddToSiriProvider); })); it('creates a new series the first time it sees an id', function () { - var series = new Map(); - var point = {}; - var id = 'id'; + let series = new Map(); + let point = {}; + let id = 'id'; addToSiri(series, point, id); expect(series.has(id)).to.be(true); @@ -22,13 +23,13 @@ describe('addToSiri', function () { }); it('adds points to existing series if id has been seen', function () { - var series = new Map(); - var id = 'id'; + let series = new Map(); + let id = 'id'; - var point = {}; + let point = {}; addToSiri(series, point, id); - var point2 = {}; + let point2 = {}; addToSiri(series, point2, id); expect(series.has(id)).to.be(true); @@ -40,10 +41,10 @@ describe('addToSiri', function () { }); it('allows overriding the series label', function () { - var series = new Map(); - var id = 'id'; - var label = 'label'; - var point = {}; + let series = new Map(); + let id = 'id'; + let label = 'label'; + let point = {}; addToSiri(series, point, id, label); expect(series.has(id)).to.be(true); diff --git a/src/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js b/src/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js index 1f7d8e338cf56..501631e1c921a 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js +++ b/src/ui/public/agg_response/point_series/__tests__/_fake_x_aspect.js @@ -1,25 +1,29 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import AggTypesAggTypeProvider from 'ui/agg_types/agg_type'; +import AggResponsePointSeriesFakeXAspectProvider from 'ui/agg_response/point_series/_fake_x_aspect'; describe('makeFakeXAspect', function () { - var makeFakeXAspect; - var Vis; - var AggType; - var AggConfig; - var indexPattern; - var expect = require('expect.js'); - var ngMock = require('ngMock'); + let makeFakeXAspect; + let Vis; + let AggType; + let AggConfig; + let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - AggType = Private(require('ui/agg_types/AggType')); - indexPattern = Private(require('ui/Vis')); - makeFakeXAspect = Private(require('ui/agg_response/point_series/_fake_x_aspect')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + AggType = Private(AggTypesAggTypeProvider); + indexPattern = Private(VisProvider); + makeFakeXAspect = Private(AggResponsePointSeriesFakeXAspectProvider); })); it('creates an object that looks like an aspect', function () { - var vis = new Vis(indexPattern, { type: 'histogram' }); - var aspect = makeFakeXAspect(vis); + let vis = new Vis(indexPattern, { type: 'histogram' }); + let aspect = makeFakeXAspect(vis); expect(aspect) .to.have.property('i', -1) diff --git a/src/ui/public/agg_response/point_series/__tests__/_get_aspects.js b/src/ui/public/agg_response/point_series/__tests__/_get_aspects.js index 9ac566e0e48bd..e1bf5ce07a702 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_get_aspects.js +++ b/src/ui/public/agg_response/point_series/__tests__/_get_aspects.js @@ -1,28 +1,33 @@ +import _ from 'lodash'; +import moment from 'moment'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import AggResponsePointSeriesAddToSiriProvider from 'ui/agg_response/point_series/_add_to_siri'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import AggResponsePointSeriesGetAspectsProvider from 'ui/agg_response/point_series/_get_aspects'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('getAspects', function () { - var _ = require('lodash'); - var moment = require('moment'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var Vis; - var Table; - var AggConfig; - var indexPattern; - var getAspects; + let Vis; + let Table; + let AggConfig; + let indexPattern; + let getAspects; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - Table = Private(require('ui/agg_response/point_series/_add_to_siri')); - AggConfig = Private(require('ui/Vis/AggConfig')); - getAspects = Private(require('ui/agg_response/point_series/_get_aspects')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + Vis = Private(VisProvider); + Table = Private(AggResponsePointSeriesAddToSiriProvider); + AggConfig = Private(VisAggConfigProvider); + getAspects = Private(AggResponsePointSeriesGetAspectsProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); - var vis; - var table; + let vis; + let table; - var date = _.memoize(function (n) { + let date = _.memoize(function (n) { return moment().startOf('day').add(n, 'hour').valueOf(); }); @@ -45,7 +50,7 @@ describe('getAspects', function () { function init(group, x, y) { // map args to indicies that should be removed - var filter = filterByIndex([ + let filter = filterByIndex([ x > 0, x > 1, group > 0, @@ -87,7 +92,7 @@ describe('getAspects', function () { ].map(filter) }; - var aggs = vis.aggs.splice(0, vis.aggs.length); + let aggs = vis.aggs.splice(0, vis.aggs.length); filter(aggs).forEach(function (filter) { vis.aggs.push(filter); }); @@ -96,7 +101,7 @@ describe('getAspects', function () { it('produces an aspect object for each of the aspect types found in the columns', function () { init(1, 1, 1); - var aspects = getAspects(vis, table); + let aspects = getAspects(vis, table); validate(aspects.x, 0); validate(aspects.series, 1); validate(aspects.y, 2); @@ -105,7 +110,7 @@ describe('getAspects', function () { it('uses arrays only when there are more than one aspect of a specific type', function () { init(0, 1, 2); - var aspects = getAspects(vis, table); + let aspects = getAspects(vis, table); validate(aspects.x, 0); expect(aspects.series == null).to.be(true); @@ -134,7 +139,7 @@ describe('getAspects', function () { it('creates a fake x aspect if the column does not exist', function () { init(0, 0, 1); - var aspects = getAspects(vis, table); + let aspects = getAspects(vis, table); expect(aspects.x) .to.be.an('object') diff --git a/src/ui/public/agg_response/point_series/__tests__/_get_point.js b/src/ui/public/agg_response/point_series/__tests__/_get_point.js index 26afb0d767506..1017a5f020cd1 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_get_point.js +++ b/src/ui/public/agg_response/point_series/__tests__/_get_point.js @@ -1,24 +1,25 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesGetPointProvider from 'ui/agg_response/point_series/_get_point'; describe('getPoint', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var getPoint; + let getPoint; - var truthFormatted = { fieldFormatter: _.constant(_.constant(true)) }; - var identFormatted = { fieldFormatter: _.constant(_.identity) }; + let truthFormatted = { fieldFormatter: _.constant(_.constant(true)) }; + let identFormatted = { fieldFormatter: _.constant(_.identity) }; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - getPoint = Private(require('ui/agg_response/point_series/_get_point')); + getPoint = Private(AggResponsePointSeriesGetPointProvider); })); describe('Without series aspect', function () { - var seriesAspect; - var xAspect; - var yAspect; - var yScale; + let seriesAspect; + let xAspect; + let yAspect; + let yScale; beforeEach(function () { seriesAspect = null; @@ -28,9 +29,9 @@ describe('getPoint', function () { }); it('properly unwraps and scales values', function () { - var row = [ { value: 1 }, { value: 2 }, { value: 3 } ]; - var zAspect = { i: 2 }; - var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect, zAspect); + let row = [ { value: 1 }, { value: 2 }, { value: 3 } ]; + let zAspect = { i: 2 }; + let point = getPoint(xAspect, seriesAspect, yScale, row, yAspect, zAspect); expect(point) .to.have.property('x', 1) @@ -41,17 +42,17 @@ describe('getPoint', function () { }); it('ignores points with a y value of NaN', function () { - var row = [ { value: 1 }, { value: 'NaN' }]; - var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); + let row = [ { value: 1 }, { value: 'NaN' }]; + let point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); expect(point).to.be(void 0); }); }); describe('With series aspect', function () { - var row; - var xAspect; - var yAspect; - var yScale; + let row; + let xAspect; + let yAspect; + let yScale; beforeEach(function () { row = [ { value: 1 }, { value: 2 }, { value: 3 }]; @@ -61,8 +62,8 @@ describe('getPoint', function () { }); it('properly unwraps and scales values', function () { - var seriesAspect = { i: 1, agg: identFormatted }; - var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); + let seriesAspect = { i: 1, agg: identFormatted }; + let point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); expect(point) .to.have.property('x', 1) @@ -72,8 +73,8 @@ describe('getPoint', function () { }); it('properly formats series values', function () { - var seriesAspect = { i: 1, agg: truthFormatted }; - var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); + let seriesAspect = { i: 1, agg: truthFormatted }; + let point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); expect(point) .to.have.property('x', 1) @@ -83,8 +84,8 @@ describe('getPoint', function () { }); it ('adds the aggConfig to the points', function () { - var seriesAspect = { i: 1, agg: truthFormatted}; - var point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); + let seriesAspect = { i: 1, agg: truthFormatted}; + let point = getPoint(xAspect, seriesAspect, yScale, row, yAspect); expect(point).to.have.property('aggConfig', truthFormatted); }); diff --git a/src/ui/public/agg_response/point_series/__tests__/_get_series.js b/src/ui/public/agg_response/point_series/__tests__/_get_series.js index c7c0bf6a2c05e..b09fd319624fd 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_get_series.js +++ b/src/ui/public/agg_response/point_series/__tests__/_get_series.js @@ -1,14 +1,15 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesGetSeriesProvider from 'ui/agg_response/point_series/_get_series'; describe('getSeries', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var getSeries; + let getSeries; - var agg = { fieldFormatter: _.constant(_.identity) }; + let agg = { fieldFormatter: _.constant(_.identity) }; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - getSeries = Private(require('ui/agg_response/point_series/_get_series')); + getSeries = Private(AggResponsePointSeriesGetSeriesProvider); })); function wrapRows(row) { @@ -18,7 +19,7 @@ describe('getSeries', function () { } it('produces a single series with points for each row', function () { - var rows = [ + let rows = [ [1, 2, 3], [1, 2, 3], [1, 2, 3], @@ -26,7 +27,7 @@ describe('getSeries', function () { [1, 2, 3] ].map(wrapRows); - var chart = { + let chart = { aspects: { x: { i: 0 }, y: { i: 1 }, @@ -34,13 +35,13 @@ describe('getSeries', function () { } }; - var series = getSeries(rows, chart); + let series = getSeries(rows, chart); expect(series) .to.be.an('array') .and.to.have.length(1); - var siri = series[0]; + let siri = series[0]; expect(siri) .to.be.an('object') .and.have.property('label', '') @@ -59,7 +60,7 @@ describe('getSeries', function () { }); it('produces multiple series if there are multiple y aspects', function () { - var rows = [ + let rows = [ [1, 2, 3], [1, 2, 3], [1, 2, 3], @@ -67,7 +68,7 @@ describe('getSeries', function () { [1, 2, 3] ].map(wrapRows); - var chart = { + let chart = { aspects: { x: { i: 0 }, y: [ @@ -77,7 +78,7 @@ describe('getSeries', function () { } }; - var series = getSeries(rows, chart); + let series = getSeries(rows, chart); expect(series) .to.be.an('array') @@ -102,7 +103,7 @@ describe('getSeries', function () { }); it('produces multiple series if there is a series aspect', function () { - var rows = [ + let rows = [ ['0', 3], ['1', 3], ['1', 'NaN'], @@ -113,7 +114,7 @@ describe('getSeries', function () { ['1', 3] ].map(wrapRows); - var chart = { + let chart = { aspects: { x: { i: -1 }, series: { i: 0, agg: agg }, @@ -121,7 +122,7 @@ describe('getSeries', function () { } }; - var series = getSeries(rows, chart); + let series = getSeries(rows, chart); expect(series) .to.be.an('array') @@ -146,7 +147,7 @@ describe('getSeries', function () { }); it('produces multiple series if there is a series aspect and multipl y aspects', function () { - var rows = [ + let rows = [ ['0', 3, 4], ['1', 3, 4], ['0', 3, 4], @@ -155,7 +156,7 @@ describe('getSeries', function () { ['1', 3, 4] ].map(wrapRows); - var chart = { + let chart = { aspects: { x: { i: -1 }, series: { i: 0, agg: agg }, @@ -166,7 +167,7 @@ describe('getSeries', function () { } }; - var series = getSeries(rows, chart); + let series = getSeries(rows, chart); expect(series) .to.be.an('array') @@ -196,7 +197,7 @@ describe('getSeries', function () { }); it('produces a series list in the same order as its corresponding metric column', function () { - var rows = [ + let rows = [ ['0', 3, 4], ['1', 3, 4], ['0', 3, 4], @@ -205,7 +206,7 @@ describe('getSeries', function () { ['1', 3, 4] ].map(wrapRows); - var chart = { + let chart = { aspects: { x: { i: -1 }, series: { i: 0, agg: agg }, @@ -216,7 +217,7 @@ describe('getSeries', function () { } }; - var series = getSeries(rows, chart); + let series = getSeries(rows, chart); expect(series[0]).to.have.property('label', '0: 0'); expect(series[1]).to.have.property('label', '0: 1'); expect(series[2]).to.have.property('label', '1: 0'); @@ -229,7 +230,7 @@ describe('getSeries', function () { y.i = i; }); - var series2 = getSeries(rows, chart); + let series2 = getSeries(rows, chart); expect(series2[0]).to.have.property('label', '0: 1'); expect(series2[1]).to.have.property('label', '0: 0'); expect(series2[2]).to.have.property('label', '1: 1'); diff --git a/src/ui/public/agg_response/point_series/__tests__/_init_x_axis.js b/src/ui/public/agg_response/point_series/__tests__/_init_x_axis.js index 904ee1c1b20d1..974afab294831 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_init_x_axis.js +++ b/src/ui/public/agg_response/point_series/__tests__/_init_x_axis.js @@ -1,16 +1,17 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesInitXAxisProvider from 'ui/agg_response/point_series/_init_x_axis'; describe('initXAxis', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var initXAxis; + let initXAxis; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - initXAxis = Private(require('ui/agg_response/point_series/_init_x_axis')); + initXAxis = Private(AggResponsePointSeriesInitXAxisProvider); })); - var baseChart = { + let baseChart = { aspects: { x: { agg: { @@ -26,7 +27,7 @@ describe('initXAxis', function () { }; it('sets the xAxisFormatter if the agg is not ordered', function () { - var chart = _.cloneDeep(baseChart); + let chart = _.cloneDeep(baseChart); initXAxis(chart); expect(chart) .to.have.property('xAxisLabel', 'label') @@ -34,7 +35,7 @@ describe('initXAxis', function () { }); it('makes the chart ordered if the agg is ordered', function () { - var chart = _.cloneDeep(baseChart); + let chart = _.cloneDeep(baseChart); chart.aspects.x.agg.type.ordered = true; initXAxis(chart); @@ -49,7 +50,7 @@ describe('initXAxis', function () { }); it('reads the interval param from the x agg', function () { - var chart = _.cloneDeep(baseChart); + let chart = _.cloneDeep(baseChart); chart.aspects.x.agg.type.ordered = true; chart.aspects.x.agg.write = _.constant({ params: { interval: 10 } }); diff --git a/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js b/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js index 0421369f95ff7..97f1c98e12487 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js +++ b/src/ui/public/agg_response/point_series/__tests__/_init_y_axis.js @@ -1,13 +1,14 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesInitYAxisProvider from 'ui/agg_response/point_series/_init_y_axis'; describe('initYAxis', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var initYAxis; + let initYAxis; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - initYAxis = Private(require('ui/agg_response/point_series/_init_y_axis')); + initYAxis = Private(AggResponsePointSeriesInitYAxisProvider); })); function agg() { @@ -18,7 +19,7 @@ describe('initYAxis', function () { }; } - var baseChart = { + let baseChart = { aspects: { y: [ { agg: agg(), col: { title: 'y1' } }, @@ -32,17 +33,17 @@ describe('initYAxis', function () { }; describe('with a single y aspect', function () { - var singleYBaseChart = _.cloneDeep(baseChart); + let singleYBaseChart = _.cloneDeep(baseChart); singleYBaseChart.aspects.y = singleYBaseChart.aspects.y[0]; it('sets the yAxisFormatter the the field formats convert fn', function () { - var chart = _.cloneDeep(singleYBaseChart); + let chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); expect(chart).to.have.property('yAxisFormatter', chart.aspects.y.agg.fieldFormatter()); }); it('sets the yAxisLabel', function () { - var chart = _.cloneDeep(singleYBaseChart); + let chart = _.cloneDeep(singleYBaseChart); initYAxis(chart); expect(chart).to.have.property('yAxisLabel', 'y1'); }); @@ -50,7 +51,7 @@ describe('initYAxis', function () { describe('with mutliple y aspects', function () { it('sets the yAxisFormatter the the field formats convert fn for the first y aspect', function () { - var chart = _.cloneDeep(baseChart); + let chart = _.cloneDeep(baseChart); initYAxis(chart); expect(chart).to.have.property('yAxisFormatter'); @@ -60,7 +61,7 @@ describe('initYAxis', function () { }); it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function () { - var chart = _.cloneDeep(baseChart); + let chart = _.cloneDeep(baseChart); initYAxis(chart); expect(chart).to.have.property('yAxisLabel', ''); }); diff --git a/src/ui/public/agg_response/point_series/__tests__/_main.js b/src/ui/public/agg_response/point_series/__tests__/_main.js index c2f1939ac4182..8eba049940350 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_main.js +++ b/src/ui/public/agg_response/point_series/__tests__/_main.js @@ -1,40 +1,44 @@ +import _ from 'lodash'; +import moment from 'moment'; +import AggConfigResult from 'ui/vis/agg_config_result'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import AggResponseTabifyTableProvider from 'ui/agg_response/tabify/_table'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggResponsePointSeriesPointSeriesProvider from 'ui/agg_response/point_series/point_series'; describe('pointSeriesChartDataFromTable', function () { this.slow(1000); - var _ = require('lodash'); - var moment = require('moment'); - var AggConfigResult = require('ui/Vis/AggConfigResult'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var pointSeriesChartDataFromTable; - var indexPattern; - var Table; - var Vis; + let pointSeriesChartDataFromTable; + let indexPattern; + let Table; + let Vis; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - Table = Private(require('ui/agg_response/tabify/_table')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - pointSeriesChartDataFromTable = Private(require('ui/agg_response/point_series/point_series')); + Vis = Private(VisProvider); + Table = Private(AggResponseTabifyTableProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + pointSeriesChartDataFromTable = Private(AggResponsePointSeriesPointSeriesProvider); })); it('handles a table with just a count', function () { - var vis = new Vis(indexPattern, { type: 'histogram' }); - var agg = vis.aggs[0]; - var result = new AggConfigResult(vis.aggs[0], void 0, 100, 100); + let vis = new Vis(indexPattern, { type: 'histogram' }); + let agg = vis.aggs[0]; + let result = new AggConfigResult(vis.aggs[0], void 0, 100, 100); - var table = new Table(); + let table = new Table(); table.columns = [ { aggConfig: agg } ]; table.rows.push([ result ]); - var chartData = pointSeriesChartDataFromTable(vis, table); + let chartData = pointSeriesChartDataFromTable(vis, table); expect(chartData).to.be.an('object'); expect(chartData.series).to.be.an('array'); expect(chartData.series).to.have.length(1); - var series = chartData.series[0]; + let series = chartData.series[0]; expect(series.values).to.have.length(1); expect(series.values[0]) .to.have.property('x', '_all') @@ -43,7 +47,7 @@ describe('pointSeriesChartDataFromTable', function () { }); it('handles a table with x and y column', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'count', schema: 'metric' }, @@ -51,32 +55,32 @@ describe('pointSeriesChartDataFromTable', function () { ] }); - var y = { + let y = { agg: vis.aggs[0], col: { aggConfig: vis.aggs[0] }, at: function (i) { return 100 * i; } }; - var x = { + let x = { agg: vis.aggs[1], col: { aggConfig: vis.aggs[1] }, at: function (i) { return moment().startOf('day').add(i, 'day').valueOf(); } }; - var rowCount = 3; - var table = new Table(); + let rowCount = 3; + let table = new Table(); table.columns = [ x.col, y.col ]; _.times(rowCount, function (i) { - var date = new AggConfigResult(x.agg, void 0, x.at(i)); + let date = new AggConfigResult(x.agg, void 0, x.at(i)); table.rows.push([date, new AggConfigResult(y.agg, date, y.at(i))]); }); - var chartData = pointSeriesChartDataFromTable(vis, table); + let chartData = pointSeriesChartDataFromTable(vis, table); expect(chartData).to.be.an('object'); expect(chartData.series).to.be.an('array'); expect(chartData.series).to.have.length(1); - var series = chartData.series[0]; + let series = chartData.series[0]; expect(series.values).to.have.length(rowCount); series.values.forEach(function (point, i) { expect(point) @@ -96,7 +100,7 @@ describe('pointSeriesChartDataFromTable', function () { }); it('handles a table with an x and two y aspects', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -105,40 +109,40 @@ describe('pointSeriesChartDataFromTable', function () { ] }); - var avg = { + let avg = { agg: vis.aggs[0], col: { title: 'average', aggConfig: vis.aggs[0] }, at: function (i) { return 75.444 * (i + 1); } }; - var date = { + let date = { agg: vis.aggs[1], col: { title: 'date', aggConfig: vis.aggs[1] }, at: function (i) { return moment().startOf('day').add(i, 'day').valueOf(); } }; - var max = { + let max = { agg: vis.aggs[2], col: { title: 'maximum', aggConfig: vis.aggs[2] }, at: function (i) { return 100 * (i + 1); } }; - var rowCount = 3; - var table = new Table(); + let rowCount = 3; + let table = new Table(); table.columns = [ date.col, avg.col, max.col ]; _.times(rowCount, function (i) { - var dateResult = new AggConfigResult(date.agg, void 0, date.at(i)); - var avgResult = new AggConfigResult(avg.agg, dateResult, avg.at(i)); - var maxResult = new AggConfigResult(max.agg, dateResult, max.at(i)); + let dateResult = new AggConfigResult(date.agg, void 0, date.at(i)); + let avgResult = new AggConfigResult(avg.agg, dateResult, avg.at(i)); + let maxResult = new AggConfigResult(max.agg, dateResult, max.at(i)); table.rows.push([dateResult, avgResult, maxResult]); }); - var chartData = pointSeriesChartDataFromTable(vis, table); + let chartData = pointSeriesChartDataFromTable(vis, table); expect(chartData).to.be.an('object'); expect(chartData.series).to.be.an('array'); expect(chartData.series).to.have.length(2); chartData.series.forEach(function (siri, i) { - var metric = i === 0 ? avg : max; + let metric = i === 0 ? avg : max; expect(siri).to.have.property('label', metric.col.label); expect(siri.values).to.have.length(rowCount); @@ -166,7 +170,7 @@ describe('pointSeriesChartDataFromTable', function () { }); it('handles a table with an x, a series, and two y aspects', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', schema: 'group', params: { field: 'extension' } }, @@ -176,53 +180,53 @@ describe('pointSeriesChartDataFromTable', function () { ] }); - var extensions = ['php', 'jpg', 'gif', 'css']; - var term = { + let extensions = ['php', 'jpg', 'gif', 'css']; + let term = { agg: vis.aggs[0], col: { title: 'extensions', aggConfig: vis.aggs[0] }, at: function (i) { return extensions[i % extensions.length]; } }; - var avg = { + let avg = { agg: vis.aggs[1], col: { title: 'average', aggConfig: vis.aggs[1] }, at: function (i) { return 75.444 * (i + 1); } }; - var date = { + let date = { agg: vis.aggs[2], col: { title: 'date', aggConfig: vis.aggs[2] }, at: function (i) { return moment().startOf('day').add(i, 'day').valueOf(); } }; - var max = { + let max = { agg: vis.aggs[3], col: { title: 'maximum', aggConfig: vis.aggs[3] }, at: function (i) { return 100 * (i + 1); } }; - var metricCount = 2; - var rowsPerSegment = 2; - var rowCount = extensions.length * rowsPerSegment; - var table = new Table(); + let metricCount = 2; + let rowsPerSegment = 2; + let rowCount = extensions.length * rowsPerSegment; + let table = new Table(); table.columns = [ date.col, term.col, avg.col, max.col ]; _.times(rowCount, function (i) { - var dateResult = new AggConfigResult(date.agg, void 0, date.at(i)); - var termResult = new AggConfigResult(term.agg, dateResult, term.at(i)); - var avgResult = new AggConfigResult(avg.agg, termResult, avg.at(i)); - var maxResult = new AggConfigResult(max.agg, termResult, max.at(i)); + let dateResult = new AggConfigResult(date.agg, void 0, date.at(i)); + let termResult = new AggConfigResult(term.agg, dateResult, term.at(i)); + let avgResult = new AggConfigResult(avg.agg, termResult, avg.at(i)); + let maxResult = new AggConfigResult(max.agg, termResult, max.at(i)); table.rows.push([dateResult, termResult, avgResult, maxResult]); }); - var chartData = pointSeriesChartDataFromTable(vis, table); + let chartData = pointSeriesChartDataFromTable(vis, table); expect(chartData).to.be.an('object'); expect(chartData.series).to.be.an('array'); // one series for each extension, and then one for each metric inside expect(chartData.series).to.have.length(extensions.length * metricCount); chartData.series.forEach(function (siri, i) { // figure out the metric used to create this series - var metricAgg = siri.values[0].aggConfigResult.aggConfig; - var metric = avg.agg === metricAgg ? avg : max; + let metricAgg = siri.values[0].aggConfigResult.aggConfig; + let metric = avg.agg === metricAgg ? avg : max; expect(siri.values).to.have.length(rowsPerSegment); siri.values.forEach(function (point) { diff --git a/src/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js b/src/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js index f25fcd8321ef0..db7763ca82772 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js +++ b/src/ui/public/agg_response/point_series/__tests__/_ordered_date_axis.js @@ -1,11 +1,12 @@ +import moment from 'moment'; +import _ from 'lodash'; +import sinon from 'auto-release-sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesOrderedDateAxisProvider from 'ui/agg_response/point_series/_ordered_date_axis'; describe('orderedDateAxis', function () { - var moment = require('moment'); - var _ = require('lodash'); - var sinon = require('auto-release-sinon'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var baseArgs = { + let baseArgs = { vis: { indexPattern: { timeFieldName: '@timestamp' @@ -27,16 +28,16 @@ describe('orderedDateAxis', function () { } }; - var orderedDateAxis; + let orderedDateAxis; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - orderedDateAxis = Private(require('ui/agg_response/point_series/_ordered_date_axis')); + orderedDateAxis = Private(AggResponsePointSeriesOrderedDateAxisProvider); })); describe('xAxisFormatter', function () { it('sets the xAxisFormatter', function () { - var args = _.cloneDeep(baseArgs); + let args = _.cloneDeep(baseArgs); orderedDateAxis(args.vis, args.chart); expect(args.chart).to.have.property('xAxisFormatter'); @@ -44,10 +45,10 @@ describe('orderedDateAxis', function () { }); it('formats values using moment, and returns strings', function () { - var args = _.cloneDeep(baseArgs); + let args = _.cloneDeep(baseArgs); orderedDateAxis(args.vis, args.chart); - var val = '2014-08-06T12:34:01'; + let val = '2014-08-06T12:34:01'; expect(args.chart.xAxisFormatter(val)) .to.be(moment(val).format('hh:mm:ss')); }); @@ -55,7 +56,7 @@ describe('orderedDateAxis', function () { describe('ordered object', function () { it('sets date: true', function () { - var args = _.cloneDeep(baseArgs); + let args = _.cloneDeep(baseArgs); orderedDateAxis(args.vis, args.chart); expect(args.chart) @@ -66,21 +67,21 @@ describe('orderedDateAxis', function () { }); it('relies on agg.buckets for the interval', function () { - var args = _.cloneDeep(baseArgs); - var spy = sinon.spy(args.chart.aspects.x.agg.buckets, 'getInterval'); + let args = _.cloneDeep(baseArgs); + let spy = sinon.spy(args.chart.aspects.x.agg.buckets, 'getInterval'); orderedDateAxis(args.vis, args.chart); expect(spy).to.have.property('callCount', 1); }); it('sets the min/max when the buckets are bounded', function () { - var args = _.cloneDeep(baseArgs); + let args = _.cloneDeep(baseArgs); orderedDateAxis(args.vis, args.chart); expect(moment.isMoment(args.chart.ordered.min)).to.be(true); expect(moment.isMoment(args.chart.ordered.max)).to.be(true); }); it('does not set the min/max when the buckets are unbounded', function () { - var args = _.cloneDeep(baseArgs); + let args = _.cloneDeep(baseArgs); args.chart.aspects.x.agg.buckets.getBounds = _.constant(); orderedDateAxis(args.vis, args.chart); expect(args.chart.ordered).to.not.have.property('min'); diff --git a/src/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js b/src/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js index 75566f842fc09..73c9e796f252b 100644 --- a/src/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js +++ b/src/ui/public/agg_response/point_series/__tests__/_tooltip_formatter.js @@ -1,14 +1,15 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponsePointSeriesTooltipFormatterProvider from 'ui/agg_response/point_series/_tooltip_formatter'; describe('tooltipFormatter', function () { - var _ = require('lodash'); - var $ = require('jquery'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var tooltipFormatter; + let tooltipFormatter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - tooltipFormatter = Private(require('ui/agg_response/point_series/_tooltip_formatter')); + tooltipFormatter = Private(AggResponsePointSeriesTooltipFormatterProvider); })); function agg(name) { @@ -22,7 +23,7 @@ describe('tooltipFormatter', function () { return $row.eq(i).text().trim(); } - var baseEvent = { + let baseEvent = { datum: { aggConfigResult: { aggConfig: agg('inner'), @@ -41,20 +42,20 @@ describe('tooltipFormatter', function () { }; it('returns html based on the mouse event', function () { - var event = _.cloneDeep(baseEvent); - var $el = $(tooltipFormatter(event)); - var $rows = $el.find('tr'); + let event = _.cloneDeep(baseEvent); + let $el = $(tooltipFormatter(event)); + let $rows = $el.find('tr'); expect($rows.size()).to.be(3); - var $row1 = $rows.eq(0).find('td'); + let $row1 = $rows.eq(0).find('td'); expect(cell($row1, 0)).to.be('inner'); expect(cell($row1, 1)).to.be('(3)'); - var $row2 = $rows.eq(1).find('td'); + let $row2 = $rows.eq(1).find('td'); expect(cell($row2, 0)).to.be('middle'); expect(cell($row2, 1)).to.be('(2)'); - var $row3 = $rows.eq(2).find('td'); + let $row3 = $rows.eq(2).find('td'); expect(cell($row3, 0)).to.be('top'); expect(cell($row3, 1)).to.be('(1)'); }); diff --git a/src/ui/public/agg_response/point_series/__tests__/point_series.js b/src/ui/public/agg_response/point_series/__tests__/point_series.js index 5e39800fa4462..b5b15c11bf1b5 100644 --- a/src/ui/public/agg_response/point_series/__tests__/point_series.js +++ b/src/ui/public/agg_response/point_series/__tests__/point_series.js @@ -1,12 +1,12 @@ +import './_main'; +import './_add_to_siri'; +import './_fake_x_aspect'; +import './_get_aspects'; +import './_get_point'; +import './_get_series'; +import './_init_x_axis'; +import './_init_y_axis'; +import './_ordered_date_axis'; +import './_tooltip_formatter'; describe('Point Series Agg Response', function () { - require('./_main'); - require('./_add_to_siri'); - require('./_fake_x_aspect'); - require('./_get_aspects'); - require('./_get_point'); - require('./_get_series'); - require('./_init_x_axis'); - require('./_init_y_axis'); - require('./_ordered_date_axis'); - require('./_tooltip_formatter'); }); diff --git a/src/ui/public/agg_response/point_series/_add_to_siri.js b/src/ui/public/agg_response/point_series/_add_to_siri.js index df50b11f6c7e5..d5ad8a84530d9 100644 --- a/src/ui/public/agg_response/point_series/_add_to_siri.js +++ b/src/ui/public/agg_response/point_series/_add_to_siri.js @@ -1,17 +1,15 @@ -define(function (require) { - return function PointSeriesAddToSiri() { - return function addToSiri(series, point, id, label) { - id = id == null ? '' : id + ''; +export default function PointSeriesAddToSiri() { + return function addToSiri(series, point, id, label) { + id = id == null ? '' : id + ''; - if (series.has(id)) { - series.get(id).values.push(point); - return; - } + if (series.has(id)) { + series.get(id).values.push(point); + return; + } - series.set(id, { - label: label == null ? id : label, - values: [point] - }); - }; + series.set(id, { + label: label == null ? id : label, + values: [point] + }); }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_fake_x_aspect.js b/src/ui/public/agg_response/point_series/_fake_x_aspect.js index 5de2c4928aff9..5f9516a421cc2 100644 --- a/src/ui/public/agg_response/point_series/_fake_x_aspect.js +++ b/src/ui/public/agg_response/point_series/_fake_x_aspect.js @@ -1,29 +1,30 @@ -define(function (require) { - return function PointSeriesFakeXAxis(Private) { - var AggConfig = Private(require('ui/Vis/AggConfig')); - var AggType = Private(require('ui/agg_types/AggType')); +import VisAggConfigProvider from 'ui/vis/agg_config'; +import AggTypesAggTypeProvider from 'ui/agg_types/agg_type'; - var allAgg = new AggType({ - name: 'all', - title: 'All docs', - ordered: false, - hasNoDsl: true - }); +export default function PointSeriesFakeXAxis(Private) { + let AggConfig = Private(VisAggConfigProvider); + let AggType = Private(AggTypesAggTypeProvider); + + let allAgg = new AggType({ + name: 'all', + title: 'All docs', + ordered: false, + hasNoDsl: true + }); - return function makeFakeXAxis(vis) { - var fake = new AggConfig(vis, { - type: allAgg, - schema: vis.type.schemas.all.byName.segment - }); + return function makeFakeXAxis(vis) { + let fake = new AggConfig(vis, { + type: allAgg, + schema: vis.type.schemas.all.byName.segment + }); - return { - i: -1, - agg: fake, - col: { - aggConfig: fake, - label: fake.makeLabel() - } - }; + return { + i: -1, + agg: fake, + col: { + aggConfig: fake, + label: fake.makeLabel() + } }; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_get_aspects.js b/src/ui/public/agg_response/point_series/_get_aspects.js index 58ce78b3549ef..f4978de298858 100644 --- a/src/ui/public/agg_response/point_series/_get_aspects.js +++ b/src/ui/public/agg_response/point_series/_get_aspects.js @@ -1,59 +1,58 @@ -define(function (require) { - return function PointSeriesGetAspects(Private) { - var _ = require('lodash'); - var fakeXAspect = Private(require('ui/agg_response/point_series/_fake_x_aspect')); - - var map = { - segment: 'x', - metric: 'y', - radius: 'z', - width: 'width', - group: 'series' - }; - - function columnToAspect(aspects, col, i) { - var schema = col.aggConfig.schema.name; +import _ from 'lodash'; +import AggResponsePointSeriesFakeXAspectProvider from 'ui/agg_response/point_series/_fake_x_aspect'; +export default function PointSeriesGetAspects(Private) { + let fakeXAspect = Private(AggResponsePointSeriesFakeXAspectProvider); + + let map = { + segment: 'x', + metric: 'y', + radius: 'z', + width: 'width', + group: 'series' + }; - var name = map[schema]; - if (!name) throw new TypeError('unknown schema name "' + schema + '"'); + function columnToAspect(aspects, col, i) { + let schema = col.aggConfig.schema.name; - var aspect = { - i: i, - col: col, - agg: col.aggConfig - }; + let name = map[schema]; + if (!name) throw new TypeError('unknown schema name "' + schema + '"'); - if (!aspects[name]) aspects[name] = []; - aspects[name].push(aspect); - } + let aspect = { + i: i, + col: col, + agg: col.aggConfig + }; - /** - * Identify and group the columns based on the aspect of the pointSeries - * they represent. - * - * @param {array} columns - the list of columns - * @return {object} - an object with a key for each aspect (see map). The values - * may be undefined, a single aspect, or an array of aspects. - */ - return function getAspects(vis, table) { - var aspects = _(table.columns) - // write each column into the aspects under it's group - .transform(columnToAspect, {}) - // unwrap groups that only have one value, and validate groups that have more - .transform(function (aspects, group, name) { - if (name !== 'y' && group.length > 1) { - throw new TypeError('Only multiple metrics are supported in point series'); - } - - aspects[name] = group.length > 1 ? group : group[0]; - }) - .value(); - - if (!aspects.x) { - aspects.x = fakeXAspect(vis); + if (!aspects[name]) aspects[name] = []; + aspects[name].push(aspect); + } + + /** + * Identify and group the columns based on the aspect of the pointSeries + * they represent. + * + * @param {array} columns - the list of columns + * @return {object} - an object with a key for each aspect (see map). The values + * may be undefined, a single aspect, or an array of aspects. + */ + return function getAspects(vis, table) { + let aspects = _(table.columns) + // write each column into the aspects under it's group + .transform(columnToAspect, {}) + // unwrap groups that only have one value, and validate groups that have more + .transform(function (aspects, group, name) { + if (name !== 'y' && group.length > 1) { + throw new TypeError('Only multiple metrics are supported in point series'); } - return aspects; - }; + aspects[name] = group.length > 1 ? group : group[0]; + }) + .value(); + + if (!aspects.x) { + aspects.x = fakeXAspect(vis); + } + + return aspects; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_get_point.js b/src/ui/public/agg_response/point_series/_get_point.js index c0b4e30ef2f0c..266ba41ce5c81 100644 --- a/src/ui/public/agg_response/point_series/_get_point.js +++ b/src/ui/public/agg_response/point_series/_get_point.js @@ -1,40 +1,38 @@ -define(function (require) { - return function PointSeriesGetPoint() { - var _ = require('lodash'); - function unwrap(aggConfigResult, def) { - return aggConfigResult ? aggConfigResult.value : def; - } +import _ from 'lodash'; +export default function PointSeriesGetPoint() { + function unwrap(aggConfigResult, def) { + return aggConfigResult ? aggConfigResult.value : def; + } - return function getPoint(x, series, yScale, row, y, z) { - var zRow = z && row[z.i]; - var xRow = row[x.i]; + return function getPoint(x, series, yScale, row, y, z) { + let zRow = z && row[z.i]; + let xRow = row[x.i]; - var point = { - x: unwrap(xRow, '_all'), - xi: xRow && xRow.$order, - y: unwrap(row[y.i]), - z: zRow && unwrap(zRow), - aggConfigResult: row[y.i], - extraMetrics: _.compact([zRow]), - yScale: yScale - }; + let point = { + x: unwrap(xRow, '_all'), + xi: xRow && xRow.$order, + y: unwrap(row[y.i]), + z: zRow && unwrap(zRow), + aggConfigResult: row[y.i], + extraMetrics: _.compact([zRow]), + yScale: yScale + }; - if (point.y === 'NaN') { - // filter out NaN from stats - // from metrics that are not based at zero - return; - } + if (point.y === 'NaN') { + // filter out NaN from stats + // from metrics that are not based at zero + return; + } - if (series) { - point.aggConfig = series.agg; - point.series = series.agg.fieldFormatter()(unwrap(row[series.i])); - } + if (series) { + point.aggConfig = series.agg; + point.series = series.agg.fieldFormatter()(unwrap(row[series.i])); + } - if (yScale) { - point.y *= yScale; - } + if (yScale) { + point.y *= yScale; + } - return point; - }; + return point; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_get_series.js b/src/ui/public/agg_response/point_series/_get_series.js index 7e86ecf67ba2f..817ec2712d96f 100644 --- a/src/ui/public/agg_response/point_series/_get_series.js +++ b/src/ui/public/agg_response/point_series/_get_series.js @@ -1,55 +1,55 @@ -define(function (require) { - return function PointSeriesGetSeries(Private) { - var _ = require('lodash'); - var getPoint = Private(require('ui/agg_response/point_series/_get_point')); - var addToSiri = Private(require('ui/agg_response/point_series/_add_to_siri')); - - return function getSeries(rows, chart) { - var aspects = chart.aspects; - var multiY = _.isArray(aspects.y); - var yScale = chart.yScale; - var partGetPoint = _.partial(getPoint, aspects.x, aspects.series, yScale); - - var series = _(rows) - .transform(function (series, row) { - if (!multiY) { - var point = partGetPoint(row, aspects.y, aspects.z); - if (point) addToSiri(series, point, point.series); - return; - } +import _ from 'lodash'; +import AggResponsePointSeriesGetPointProvider from 'ui/agg_response/point_series/_get_point'; +import AggResponsePointSeriesAddToSiriProvider from 'ui/agg_response/point_series/_add_to_siri'; +export default function PointSeriesGetSeries(Private) { + let getPoint = Private(AggResponsePointSeriesGetPointProvider); + let addToSiri = Private(AggResponsePointSeriesAddToSiriProvider); + + return function getSeries(rows, chart) { + let aspects = chart.aspects; + let multiY = _.isArray(aspects.y); + let yScale = chart.yScale; + let partGetPoint = _.partial(getPoint, aspects.x, aspects.series, yScale); + + let series = _(rows) + .transform(function (series, row) { + if (!multiY) { + let point = partGetPoint(row, aspects.y, aspects.z); + if (point) addToSiri(series, point, point.series); + return; + } - aspects.y.forEach(function (y) { - var point = partGetPoint(row, y, aspects.z); - if (!point) return; + aspects.y.forEach(function (y) { + let point = partGetPoint(row, y, aspects.z); + if (!point) return; - var prefix = point.series ? point.series + ': ' : ''; - var seriesId = prefix + y.agg.id; - var seriesLabel = prefix + y.col.title; + let prefix = point.series ? point.series + ': ' : ''; + let seriesId = prefix + y.agg.id; + let seriesLabel = prefix + y.col.title; - addToSiri(series, point, seriesId, seriesLabel); - }); + addToSiri(series, point, seriesId, seriesLabel); + }); - }, new Map()) - .thru(series => [...series.values()]) - .value(); + }, new Map()) + .thru(series => [...series.values()]) + .value(); - if (multiY) { - series = _.sortBy(series, function (siri) { - var firstVal = siri.values[0]; - var y; + if (multiY) { + series = _.sortBy(series, function (siri) { + let firstVal = siri.values[0]; + let y; - if (firstVal) { - var agg = firstVal.aggConfigResult.aggConfig; - y = _.find(aspects.y, function (y) { - return y.agg === agg; - }); - } + if (firstVal) { + let agg = firstVal.aggConfigResult.aggConfig; + y = _.find(aspects.y, function (y) { + return y.agg === agg; + }); + } - return y ? y.i : series.length; - }); - } + return y ? y.i : series.length; + }); + } - return series; - }; + return series; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_init_x_axis.js b/src/ui/public/agg_response/point_series/_init_x_axis.js index 3910621f701c5..2a3584c3834f7 100644 --- a/src/ui/public/agg_response/point_series/_init_x_axis.js +++ b/src/ui/public/agg_response/point_series/_init_x_axis.js @@ -1,14 +1,14 @@ define(function () { return function PointSeriesInitX() { return function initXAxis(chart) { - var x = chart.aspects.x; + let x = chart.aspects.x; chart.xAxisFormatter = x.agg ? x.agg.fieldFormatter() : String; chart.xAxisLabel = x.col.title; if (!x.agg || !x.agg.type.ordered) return; chart.ordered = {}; - var xAggOutput = x.agg.write(); + let xAggOutput = x.agg.write(); if (xAggOutput.params.interval) { chart.ordered.interval = xAggOutput.params.interval; } diff --git a/src/ui/public/agg_response/point_series/_init_y_axis.js b/src/ui/public/agg_response/point_series/_init_y_axis.js index b47ec2ef884c2..edd3060fc85c2 100644 --- a/src/ui/public/agg_response/point_series/_init_y_axis.js +++ b/src/ui/public/agg_response/point_series/_init_y_axis.js @@ -1,22 +1,20 @@ -define(function (require) { - return function PointSeriesInitYAxis() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function PointSeriesInitYAxis() { - return function initYAxis(chart) { - var y = chart.aspects.y; - var x = chart.aspects.x; + return function initYAxis(chart) { + let y = chart.aspects.y; + let x = chart.aspects.x; - if (_.isArray(y)) { - // TODO: vis option should allow choosing this format - chart.yAxisFormatter = y[0].agg.fieldFormatter(); - chart.yAxisLabel = ''; // use the legend - } else { - chart.yAxisFormatter = y.agg.fieldFormatter(); - chart.yAxisLabel = y.col.title; - } + if (_.isArray(y)) { + // TODO: vis option should allow choosing this format + chart.yAxisFormatter = y[0].agg.fieldFormatter(); + chart.yAxisLabel = ''; // use the legend + } else { + chart.yAxisFormatter = y.agg.fieldFormatter(); + chart.yAxisLabel = y.col.title; + } - var xAggOutput = x.agg.write(); - chart.yScale = xAggOutput.metricScale || null; - }; + let xAggOutput = x.agg.write(); + chart.yScale = xAggOutput.metricScale || null; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_ordered_date_axis.js b/src/ui/public/agg_response/point_series/_ordered_date_axis.js index 805bea1c43bbe..08b1d962252f2 100644 --- a/src/ui/public/agg_response/point_series/_ordered_date_axis.js +++ b/src/ui/public/agg_response/point_series/_ordered_date_axis.js @@ -1,29 +1,27 @@ -define(function (require) { - return function PointSeriesOrderedDateAxis(timefilter) { - var moment = require('moment'); +import moment from 'moment'; +export default function PointSeriesOrderedDateAxis(timefilter) { - return function orderedDateAxis(vis, chart) { - var xAgg = chart.aspects.x.agg; - var buckets = xAgg.buckets; - var format = buckets.getScaledDateFormat(); + return function orderedDateAxis(vis, chart) { + let xAgg = chart.aspects.x.agg; + let buckets = xAgg.buckets; + let format = buckets.getScaledDateFormat(); - chart.xAxisFormatter = function (val) { - return moment(val).format(format); - }; - - chart.ordered = { - date: true, - interval: buckets.getInterval(), - }; + chart.xAxisFormatter = function (val) { + return moment(val).format(format); + }; - var axisOnTimeField = xAgg.fieldIsTimeField(); - var bounds = buckets.getBounds(); - if (bounds && axisOnTimeField) { - chart.ordered.min = bounds.min; - chart.ordered.max = bounds.max; - } else { - chart.ordered.endzones = false; - } + chart.ordered = { + date: true, + interval: buckets.getInterval(), }; + + let axisOnTimeField = xAgg.fieldIsTimeField(); + let bounds = buckets.getBounds(); + if (bounds && axisOnTimeField) { + chart.ordered.min = bounds.min; + chart.ordered.max = bounds.max; + } else { + chart.ordered.endzones = false; + } }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/_tooltip.html b/src/ui/public/agg_response/point_series/_tooltip.html index 9e1a52df59fb8..322d5a0bc3798 100644 --- a/src/ui/public/agg_response/point_series/_tooltip.html +++ b/src/ui/public/agg_response/point_series/_tooltip.html @@ -1,11 +1,11 @@ - - + -
    {{detail.label}} + {{detail.label}} {{detail.value}} ({{detail.percent}})
    \ No newline at end of file + diff --git a/src/ui/public/agg_response/point_series/_tooltip_formatter.js b/src/ui/public/agg_response/point_series/_tooltip_formatter.js index 97345117543a3..715d692127d1e 100644 --- a/src/ui/public/agg_response/point_series/_tooltip_formatter.js +++ b/src/ui/public/agg_response/point_series/_tooltip_formatter.js @@ -1,45 +1,43 @@ -define(function (require) { - return function PointSeriesTooltipFormatter($compile, $rootScope) { - var $ = require('jquery'); - - var $tooltipScope = $rootScope.$new(); - var $tooltip = $(require('ui/agg_response/point_series/_tooltip.html')); - $compile($tooltip)($tooltipScope); - - return function tooltipFormatter(event) { - var datum = event.datum; - if (!datum || !datum.aggConfigResult) return ''; - - var details = $tooltipScope.details = []; - var result = { $parent: datum.aggConfigResult }; - - function addDetail(result) { - var agg = result.aggConfig; - var value = result.value; - - var detail = { - value: agg.fieldFormatter()(value), - label: agg.makeLabel() - }; - - if (agg === datum.aggConfigResult.aggConfig) { - detail.percent = event.percent; - if (datum.yScale != null) { - detail.value = agg.fieldFormatter()(value * datum.yScale); - } - } +import $ from 'jquery'; +export default function PointSeriesTooltipFormatter($compile, $rootScope) { - details.push(detail); - } + let $tooltipScope = $rootScope.$new(); + let $tooltip = $(require('ui/agg_response/point_series/_tooltip.html')); + $compile($tooltip)($tooltipScope); + + return function tooltipFormatter(event) { + let datum = event.datum; + if (!datum || !datum.aggConfigResult) return ''; + + let details = $tooltipScope.details = []; + let result = { $parent: datum.aggConfigResult }; + + function addDetail(result) { + let agg = result.aggConfig; + let value = result.value; - datum.extraMetrics.forEach(addDetail); - while ((result = result.$parent) && result.aggConfig) { - addDetail(result); + let detail = { + value: agg.fieldFormatter()(value), + label: agg.makeLabel() + }; + + if (agg === datum.aggConfigResult.aggConfig) { + detail.percent = event.percent; + if (datum.yScale != null) { + detail.value = agg.fieldFormatter()(value * datum.yScale); + } } + details.push(detail); + } + + datum.extraMetrics.forEach(addDetail); + while ((result = result.$parent) && result.aggConfig) { + addDetail(result); + } + - $tooltipScope.$apply(); - return $tooltip[0].outerHTML; - }; + $tooltipScope.$apply(); + return $tooltip[0].outerHTML; }; -}); +}; diff --git a/src/ui/public/agg_response/point_series/point_series.js b/src/ui/public/agg_response/point_series/point_series.js index ddaf2c9beef21..0a2ccd90729c4 100644 --- a/src/ui/public/agg_response/point_series/point_series.js +++ b/src/ui/public/agg_response/point_series/point_series.js @@ -1,32 +1,36 @@ -define(function (require) { - return function PointSeriesProvider(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import AggResponsePointSeriesGetSeriesProvider from 'ui/agg_response/point_series/_get_series'; +import AggResponsePointSeriesGetAspectsProvider from 'ui/agg_response/point_series/_get_aspects'; +import AggResponsePointSeriesInitYAxisProvider from 'ui/agg_response/point_series/_init_y_axis'; +import AggResponsePointSeriesInitXAxisProvider from 'ui/agg_response/point_series/_init_x_axis'; +import AggResponsePointSeriesOrderedDateAxisProvider from 'ui/agg_response/point_series/_ordered_date_axis'; +import AggResponsePointSeriesTooltipFormatterProvider from 'ui/agg_response/point_series/_tooltip_formatter'; +export default function PointSeriesProvider(Private) { - var getSeries = Private(require('ui/agg_response/point_series/_get_series')); - var getAspects = Private(require('ui/agg_response/point_series/_get_aspects')); - var initYAxis = Private(require('ui/agg_response/point_series/_init_y_axis')); - var initXAxis = Private(require('ui/agg_response/point_series/_init_x_axis')); - var setupOrderedDateXAxis = Private(require('ui/agg_response/point_series/_ordered_date_axis')); - var tooltipFormatter = Private(require('ui/agg_response/point_series/_tooltip_formatter')); + let getSeries = Private(AggResponsePointSeriesGetSeriesProvider); + let getAspects = Private(AggResponsePointSeriesGetAspectsProvider); + let initYAxis = Private(AggResponsePointSeriesInitYAxisProvider); + let initXAxis = Private(AggResponsePointSeriesInitXAxisProvider); + let setupOrderedDateXAxis = Private(AggResponsePointSeriesOrderedDateAxisProvider); + let tooltipFormatter = Private(AggResponsePointSeriesTooltipFormatterProvider); - return function pointSeriesChartDataFromTable(vis, table) { - var chart = {}; - var aspects = chart.aspects = getAspects(vis, table); + return function pointSeriesChartDataFromTable(vis, table) { + let chart = {}; + let aspects = chart.aspects = getAspects(vis, table); - chart.tooltipFormatter = tooltipFormatter; + chart.tooltipFormatter = tooltipFormatter; - initXAxis(chart); - initYAxis(chart); + initXAxis(chart); + initYAxis(chart); - var datedX = aspects.x.agg.type.ordered && aspects.x.agg.type.ordered.date; - if (datedX) { - setupOrderedDateXAxis(vis, chart); - } + let datedX = aspects.x.agg.type.ordered && aspects.x.agg.type.ordered.date; + if (datedX) { + setupOrderedDateXAxis(vis, chart); + } - chart.series = getSeries(table.rows, chart); + chart.series = getSeries(table.rows, chart); - delete chart.aspects; - return chart; - }; + delete chart.aspects; + return chart; }; -}); +}; diff --git a/src/ui/public/agg_response/tabify/__tests__/_buckets.js b/src/ui/public/agg_response/tabify/__tests__/_buckets.js index f019564d93853..e2ecc8e75d55d 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_buckets.js +++ b/src/ui/public/agg_response/tabify/__tests__/_buckets.js @@ -1,23 +1,24 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyBucketsProvider from 'ui/agg_response/tabify/_buckets'; describe('Buckets wrapper', function () { - var Buckets; - var expect = require('expect.js'); - var ngMock = require('ngMock'); + let Buckets; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - Buckets = Private(require('ui/agg_response/tabify/_buckets')); + Buckets = Private(AggResponseTabifyBucketsProvider); })); function test(aggResp, count, keys) { it('reads the length', function () { - var buckets = new Buckets(aggResp); + let buckets = new Buckets(aggResp); expect(buckets).to.have.length(count); }); it('itterates properly, passing in the key', function () { - var buckets = new Buckets(aggResp); - var keysSent = []; + let buckets = new Buckets(aggResp); + let keysSent = []; buckets.forEach(function (bucket, key) { keysSent.push(key); }); @@ -28,7 +29,7 @@ describe('Buckets wrapper', function () { } describe('with object style buckets', function () { - var aggResp = { + let aggResp = { buckets: { '0-100': {}, '100-200': {}, @@ -36,14 +37,14 @@ describe('Buckets wrapper', function () { } }; - var count = 3; - var keys = ['0-100', '100-200', '200-300']; + let count = 3; + let keys = ['0-100', '100-200', '200-300']; test(aggResp, count, keys); }); describe('with array style buckets', function () { - var aggResp = { + let aggResp = { buckets: [ { key: '0-100', value: {} }, { key: '100-200', value: {} }, @@ -51,8 +52,8 @@ describe('Buckets wrapper', function () { ] }; - var count = 3; - var keys = ['0-100', '100-200', '200-300']; + let count = 3; + let keys = ['0-100', '100-200', '200-300']; test(aggResp, count, keys); }); diff --git a/src/ui/public/agg_response/tabify/__tests__/_get_columns.js b/src/ui/public/agg_response/tabify/__tests__/_get_columns.js index 136ad133d362c..d8db8717c2dc0 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_get_columns.js +++ b/src/ui/public/agg_response/tabify/__tests__/_get_columns.js @@ -1,23 +1,26 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyGetColumnsProvider from 'ui/agg_response/tabify/_get_columns'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('get columns', function () { - var getColumns; - var Vis; - var indexPattern; - var expect = require('expect.js'); - var ngMock = require('ngMock'); + let getColumns; + let Vis; + let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - getColumns = Private(require('ui/agg_response/tabify/_get_columns')); - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + getColumns = Private(AggResponseTabifyGetColumnsProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); it('should inject a count metric if no aggs exist', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie' }); while (vis.aggs.length) vis.aggs.pop(); - var columns = getColumns(vis); + let columns = getColumns(vis); expect(columns).to.have.length(1); expect(columns[0]).to.have.property('aggConfig'); @@ -25,14 +28,14 @@ describe('get columns', function () { }); it('should inject a count metric if only buckets exist', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } } ] }); - var columns = getColumns(vis); + let columns = getColumns(vis); expect(columns).to.have.length(2); expect(columns[1]).to.have.property('aggConfig'); @@ -40,7 +43,7 @@ describe('get columns', function () { }); it('should inject the metric after each bucket if the vis is hierarchical', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, @@ -50,7 +53,7 @@ describe('get columns', function () { ] }); - var columns = getColumns(vis); + let columns = getColumns(vis); expect(columns).to.have.length(8); columns.forEach(function (column, i) { @@ -60,7 +63,7 @@ describe('get columns', function () { }); it('should inject the multiple metrics after each bucket if the vis is hierarchical', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, @@ -72,7 +75,7 @@ describe('get columns', function () { ] }); - var columns = getColumns(vis); + let columns = getColumns(vis); function checkColumns(column, i) { expect(column).to.have.property('aggConfig'); @@ -90,14 +93,14 @@ describe('get columns', function () { } expect(columns).to.have.length(12); - for (var i = 0; i < columns.length; i += 3) { - var counts = { buckets: 0, metrics: 0 }; + for (let i = 0; i < columns.length; i += 3) { + let counts = { buckets: 0, metrics: 0 }; columns.slice(i, i + 3).forEach(checkColumns); } }); it('should put all metrics at the end of the columns if the vis is not hierarchical', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, @@ -109,7 +112,7 @@ describe('get columns', function () { ] }); - var columns = getColumns(vis); + let columns = getColumns(vis); expect(columns).to.have.length(6); // sum should be last diff --git a/src/ui/public/agg_response/tabify/__tests__/_integration.js b/src/ui/public/agg_response/tabify/__tests__/_integration.js index fec883f229585..ea5d6096219d6 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_integration.js +++ b/src/ui/public/agg_response/tabify/__tests__/_integration.js @@ -1,19 +1,22 @@ +import _ from 'lodash'; +import fixtures from 'fixtures/fake_hierarchical_data'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('tabifyAggResponse Integration', function () { - var _ = require('lodash'); - var fixtures = require('fixtures/fake_hierarchical_data'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var Vis; - var Buckets; - var indexPattern; - var tabifyAggResponse; + let Vis; + let Buckets; + let indexPattern; + let tabifyAggResponse; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); function normalizeIds(vis) { @@ -23,13 +26,13 @@ describe('tabifyAggResponse Integration', function () { } it('transforms a simple response properly', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); normalizeIds(vis); - var resp = tabifyAggResponse(vis, fixtures.metricOnly, { canSplit: false }); + let resp = tabifyAggResponse(vis, fixtures.metricOnly, { canSplit: false }); expect(resp).to.not.have.property('tables'); expect(resp).to.have.property('rows').and.property('columns'); @@ -43,12 +46,12 @@ describe('tabifyAggResponse Integration', function () { describe('transforms a complex response', function () { this.slow(1000); - var vis; - var avg; - var ext; - var src; - var os; - var esResp; + let vis; + let avg; + let ext; + let src; + let os; + let esResp; beforeEach(function () { vis = new Vis(indexPattern, { @@ -78,7 +81,7 @@ describe('tabifyAggResponse Integration', function () { function expectRootGroup(rootTableGroup, expectInnerTables) { expect(rootTableGroup).to.have.property('tables'); - var tables = rootTableGroup.tables; + let tables = rootTableGroup.tables; expect(tables).to.be.an('array').and.have.length(3); expectExtensionSplit(tables[0], 'png', expectInnerTables); expectExtensionSplit(tables[1], 'css', expectInnerTables); @@ -152,7 +155,7 @@ describe('tabifyAggResponse Integration', function () { // only complete rows, and only put the metrics at the end. vis.isHierarchical = _.constant(false); - var tabbed = tabifyAggResponse(vis, esResp); + let tabbed = tabifyAggResponse(vis, esResp); expectRootGroup(tabbed, function expectTable(table, splitKey) { expectColumns(table, [src, os, avg]); @@ -178,7 +181,7 @@ describe('tabifyAggResponse Integration', function () { // the existing bucket and it's metric vis.isHierarchical = _.constant(true); - var tabbed = tabifyAggResponse(vis, esResp, { + let tabbed = tabifyAggResponse(vis, esResp, { partialRows: true }); @@ -212,7 +215,7 @@ describe('tabifyAggResponse Integration', function () { // the end vis.isHierarchical = _.constant(true); - var tabbed = tabifyAggResponse(vis, esResp, { + let tabbed = tabifyAggResponse(vis, esResp, { partialRows: true, minimalColumns: true }); @@ -244,7 +247,7 @@ describe('tabifyAggResponse Integration', function () { // create metric columns after each bucket vis.isHierarchical = _.constant(false); - var tabbed = tabifyAggResponse(vis, esResp, { + let tabbed = tabifyAggResponse(vis, esResp, { minimalColumns: false }); 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 0b5ab94190f4f..5f81ef8190d68 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ b/src/ui/public/agg_response/tabify/__tests__/_response_writer.js @@ -1,17 +1,23 @@ +import _ from 'lodash'; +import sinon from 'auto-release-sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyResponseWriterProvider from 'ui/agg_response/tabify/_response_writer'; +import AggResponseTabifyTableGroupProvider from 'ui/agg_response/tabify/_table_group'; +import AggResponseTabifyBucketsProvider from 'ui/agg_response/tabify/_buckets'; +import AggResponseTabifyTableProvider from 'ui/agg_response/tabify/_table'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('ResponseWriter class', function () { - var _ = require('lodash'); - var sinon = require('auto-release-sinon'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - - var Vis; - var Table; - var Buckets; - var Private; - var TableGroup; - var getColumns; - var indexPattern; - var ResponseWriter; + + let Vis; + let Table; + let Buckets; + let Private; + let TableGroup; + let getColumns; + let indexPattern; + let ResponseWriter; function defineSetup(stubGetColumns) { beforeEach(ngMock.module('kibana')); @@ -23,12 +29,12 @@ describe('ResponseWriter class', function () { Private.stub(require('ui/agg_response/tabify/_get_columns'), getColumns); } - ResponseWriter = Private(require('ui/agg_response/tabify/_response_writer')); - TableGroup = Private(require('ui/agg_response/tabify/_table_group')); - Buckets = Private(require('ui/agg_response/tabify/_buckets')); - Table = Private(require('ui/agg_response/tabify/_table')); - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + ResponseWriter = Private(AggResponseTabifyResponseWriterProvider); + TableGroup = Private(AggResponseTabifyTableGroupProvider); + Buckets = Private(AggResponseTabifyBucketsProvider); + Table = Private(AggResponseTabifyTableProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); } @@ -36,15 +42,15 @@ describe('ResponseWriter class', function () { defineSetup(true); it('gets the columns for the vis', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis); expect(getColumns).to.have.property('callCount', 1); expect(getColumns.firstCall.args[0]).to.be(vis); }); it('collects the aggConfigs from each column in aggStack', function () { - var aggs = [ + let aggs = [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } }, { type: 'terms', schema: 'segment', params: { field: 'extension' } }, { type: 'avg', schema: 'metric', params: { field: '@timestamp' } } @@ -54,12 +60,12 @@ describe('ResponseWriter class', function () { return { aggConfig: agg }; })); - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: aggs }); - var writer = new ResponseWriter(vis); + let writer = new ResponseWriter(vis); expect(writer.aggStack).to.be.an('array'); expect(writer.aggStack).to.have.length(aggs.length); writer.aggStack.forEach(function (agg, i) { @@ -68,40 +74,40 @@ describe('ResponseWriter class', function () { }); it('sets canSplit=true by default', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis); expect(writer).to.have.property('canSplit', true); }); it('sets canSplit=false when config says to', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis, { canSplit: false }); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis, { canSplit: false }); expect(writer).to.have.property('canSplit', false); }); describe('sets partialRows', function () { it('to the value of the config if set', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var partial = Boolean(Math.round(Math.random())); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let partial = Boolean(Math.round(Math.random())); - var writer = new ResponseWriter(vis, { partialRows: partial }); + let writer = new ResponseWriter(vis, { partialRows: partial }); expect(writer).to.have.property('partialRows', partial); }); it('to the value of vis.isHierarchical if no config', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var hierarchical = Boolean(Math.round(Math.random())); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let hierarchical = Boolean(Math.round(Math.random())); sinon.stub(vis, 'isHierarchical').returns(hierarchical); - var writer = new ResponseWriter(vis, {}); + let writer = new ResponseWriter(vis, {}); expect(writer).to.have.property('partialRows', hierarchical); }); }); it('starts off with a root TableGroup', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let writer = new ResponseWriter(vis); expect(writer.root).to.be.a(TableGroup); expect(writer.splitStack).to.be.an('array'); expect(writer.splitStack).to.have.length(1); @@ -114,29 +120,29 @@ describe('ResponseWriter class', function () { describe('#response()', function () { it('returns the root TableGroup if splitting', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis); expect(writer.response()).to.be(writer.root); }); it('returns the first table if not splitting', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis, { canSplit: false }); - var table = writer._table(); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis, { canSplit: false }); + let table = writer._table(); expect(writer.response()).to.be(table); }); it('adds columns to all of the tables', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', params: { field: '_type' }, schema: 'split' }, { type: 'count', schema: 'metric' } ] }); - var buckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); - var writer = new ResponseWriter(vis); - var tables = []; + let buckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); + let writer = new ResponseWriter(vis); + let tables = []; writer.split(vis.aggs[0], buckets, function () { writer.cell(vis.aggs[1], 100, function () { @@ -148,11 +154,11 @@ describe('ResponseWriter class', function () { expect(table.columns == null).to.be(true); }); - var resp = writer.response(); + let resp = writer.response(); expect(resp).to.be.a(TableGroup); expect(resp.tables).to.have.length(2); - var nginx = resp.tables.shift(); + let nginx = resp.tables.shift(); expect(nginx).to.have.property('aggConfig', vis.aggs[0]); expect(nginx).to.have.property('key', 'nginx'); expect(nginx.tables).to.have.length(1); @@ -160,7 +166,7 @@ describe('ResponseWriter class', function () { expect(_.contains(tables, table)).to.be(true); }); - var apache = resp.tables.shift(); + let apache = resp.tables.shift(); expect(apache).to.have.property('aggConfig', vis.aggs[0]); expect(apache).to.have.property('key', 'apache'); expect(apache.tables).to.have.length(1); @@ -178,16 +184,16 @@ describe('ResponseWriter class', function () { describe('#split()', function () { it('with break if the user has specified that splitting is to be disabled', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', schema: 'split', params: { field: '_type' } }, { type: 'count', schema: 'metric' } ] }); - var agg = vis.aggs.bySchemaName.split[0]; - var buckets = new Buckets({ buckets: [ { key: 'apache' } ]}); - var writer = new ResponseWriter(vis, { canSplit: false }); + let agg = vis.aggs.bySchemaName.split[0]; + let buckets = new Buckets({ buckets: [ { key: 'apache' } ]}); + let writer = new ResponseWriter(vis, { canSplit: false }); expect(function () { writer.split(agg, buckets, _.noop); @@ -195,7 +201,7 @@ describe('ResponseWriter class', function () { }); it('forks the acrStack and rewrites the parents', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', params: { field: 'extension' }, schema: 'segment' }, @@ -205,10 +211,10 @@ describe('ResponseWriter class', function () { ] }); - var writer = new ResponseWriter(vis, { asAggConfigResults: true }); - var extensions = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] }); - var types = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); - var os = new Buckets({ buckets: [ { key: 'window' }, { key: 'osx' } ] }); + let writer = new ResponseWriter(vis, { asAggConfigResults: true }); + let extensions = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] }); + let types = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); + let os = new Buckets({ buckets: [ { key: 'window' }, { key: 'osx' } ] }); extensions.forEach(function (b, extension) { writer.cell(vis.aggs[0], extension, function () { @@ -224,11 +230,11 @@ describe('ResponseWriter class', function () { }); }); - var tables = _.flattenDeep(_.pluck(writer.response().tables, 'tables')); + let tables = _.flattenDeep(_.pluck(writer.response().tables, 'tables')); expect(tables.length).to.be(types.length); // collect the far left acr from each table - var leftAcrs = _.pluck(tables, 'rows[0][0]'); + let leftAcrs = _.pluck(tables, 'rows[0][0]'); leftAcrs.forEach(function (acr, i, acrs) { expect(acr.aggConfig).to.be(vis.aggs[0]); @@ -237,7 +243,7 @@ describe('ResponseWriter class', function () { // for all but the last acr, compare to the next if (i + 1 >= acrs.length) return; - var acr2 = leftAcrs[i + 1]; + let acr2 = leftAcrs[i + 1]; expect(acr.key).to.be(acr2.key); expect(acr.value).to.be(acr2.value); @@ -252,8 +258,8 @@ describe('ResponseWriter class', function () { describe('#cell()', function () { it('logs a cell in the ResponseWriters row buffer, calls the block arg, then removes the value from the buffer', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis); expect(writer.rowBuffer).to.have.length(0); writer.cell({}, 500, function () { @@ -266,10 +272,10 @@ describe('ResponseWriter class', function () { describe('#row()', function () { it('writes the ResponseWriters internal rowBuffer into a table', function () { - var vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); - var writer = new ResponseWriter(vis); + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [] }); + let writer = new ResponseWriter(vis); - var table = writer._table(); + let table = writer._table(); writer.cell({}, 1, function () { writer.cell({}, 2, function () { writer.cell({}, 3, function () { @@ -283,7 +289,7 @@ describe('ResponseWriter class', function () { }); it('always writes to the table group at the top of the split stack', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', schema: 'split', params: { field: '_type' } }, @@ -292,20 +298,20 @@ describe('ResponseWriter class', function () { { type: 'count', schema: 'metric' } ] }); - var splits = vis.aggs.bySchemaName.split; + let splits = vis.aggs.bySchemaName.split; - var type = splits[0]; - var typeBuckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); + let type = splits[0]; + let typeBuckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] }); - var ext = splits[1]; - var extBuckets = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] }); + let ext = splits[1]; + let extBuckets = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] }); - var os = splits[2]; - var osBuckets = new Buckets({ buckets: [ { key: 'windows' }, { key: 'mac' } ] }); + let os = splits[2]; + let osBuckets = new Buckets({ buckets: [ { key: 'windows' }, { key: 'mac' } ] }); - var count = vis.aggs[3]; + let count = vis.aggs[3]; - var writer = new ResponseWriter(vis); + let writer = new ResponseWriter(vis); writer.split(type, typeBuckets, function () { writer.split(ext, extBuckets, function () { writer.split(os, osBuckets, function (bucket, key) { @@ -316,9 +322,9 @@ describe('ResponseWriter class', function () { }); }); - var resp = writer.response(); - var sum = 0; - var tables = 0; + let resp = writer.response(); + let sum = 0; + let tables = 0; (function recurse(t) { if (t.tables) { // table group @@ -341,7 +347,7 @@ describe('ResponseWriter class', function () { }); it('writes partial rows for hierarchical vis', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'terms', schema: 'segment', params: { field: '_type' }}, @@ -349,8 +355,8 @@ describe('ResponseWriter class', function () { ] }); - var writer = new ResponseWriter(vis); - var table = writer._table(); + let writer = new ResponseWriter(vis); + let table = writer._table(); writer.cell(vis.aggs[0], 'apache', function () { writer.row(); }); @@ -360,7 +366,7 @@ describe('ResponseWriter class', function () { }); it('skips partial rows for non-hierarchical vis', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', schema: 'segment', params: { field: '_type' }}, @@ -368,8 +374,8 @@ describe('ResponseWriter class', function () { ] }); - var writer = new ResponseWriter(vis); - var table = writer._table(); + let writer = new ResponseWriter(vis); + let table = writer._table(); writer.cell(vis.aggs[0], 'apache', function () { writer.row(); }); diff --git a/src/ui/public/agg_response/tabify/__tests__/_table.js b/src/ui/public/agg_response/tabify/__tests__/_table.js index d65a93b1e906e..1e4a869bcc229 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_table.js +++ b/src/ui/public/agg_response/tabify/__tests__/_table.js @@ -1,26 +1,27 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyTableProvider from 'ui/agg_response/tabify/_table'; describe('Table class', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var Table; + let Table; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - Table = Private(require('ui/agg_response/tabify/_table')); + Table = Private(AggResponseTabifyTableProvider); })); it('exposes rows array, but not the columns', function () { - var table = new Table(); + let table = new Table(); expect(table.rows).to.be.an('array'); expect(table.columns == null).to.be.ok(); }); describe('#aggConfig', function () { it('accepts a column from the table and returns its agg config', function () { - var table = new Table(); - var football = {}; - var column = { + let table = new Table(); + let football = {}; + let column = { aggConfig: football }; @@ -29,7 +30,7 @@ describe('Table class', function () { it('throws a TypeError if the column is malformed', function () { expect(function () { - var notAColumn = {}; + let notAColumn = {}; (new Table()).aggConfig(notAColumn); }).to.throwException(TypeError); }); @@ -37,12 +38,12 @@ describe('Table class', function () { describe('#title', function () { it('returns nothing if the table is not part of a table group', function () { - var table = new Table(); + let table = new Table(); expect(table.title()).to.be(''); }); it('returns the title of the TableGroup if the table is part of one', function () { - var table = new Table(); + let table = new Table(); table.$parent = { title: 'TableGroup Title', tables: [table] @@ -54,9 +55,9 @@ describe('Table class', function () { describe('#field', function () { it('calls the columns aggConfig#field() method', function () { - var table = new Table(); - var football = {}; - var column = { + let table = new Table(); + let football = {}; + let column = { aggConfig: { field: _.constant(football) } @@ -68,9 +69,9 @@ describe('Table class', function () { describe('#fieldFormatter', function () { it('calls the columns aggConfig#fieldFormatter() method', function () { - var table = new Table(); - var football = {}; - var column = { + let table = new Table(); + let football = {}; + let column = { aggConfig: { fieldFormatter: _.constant(football) } diff --git a/src/ui/public/agg_response/tabify/__tests__/_table_group.js b/src/ui/public/agg_response/tabify/__tests__/_table_group.js index ead9e4e41e5ea..5455caa143f34 100644 --- a/src/ui/public/agg_response/tabify/__tests__/_table_group.js +++ b/src/ui/public/agg_response/tabify/__tests__/_table_group.js @@ -1,15 +1,16 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggResponseTabifyTableGroupProvider from 'ui/agg_response/tabify/_table_group'; describe('Table Group class', function () { - var TableGroup; - var expect = require('expect.js'); - var ngMock = require('ngMock'); + let TableGroup; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - TableGroup = Private(require('ui/agg_response/tabify/_table_group')); + TableGroup = Private(AggResponseTabifyTableGroupProvider); })); it('exposes tables array and empty aggConfig, key and title', function () { - var tableGroup = new TableGroup(); + let tableGroup = new TableGroup(); expect(tableGroup.tables).to.be.an('array'); expect(tableGroup.aggConfig).to.be(null); expect(tableGroup.key).to.be(null); diff --git a/src/ui/public/agg_response/tabify/__tests__/tabify.js b/src/ui/public/agg_response/tabify/__tests__/tabify.js index 9981cfb5c6cda..9c528fb61e660 100644 --- a/src/ui/public/agg_response/tabify/__tests__/tabify.js +++ b/src/ui/public/agg_response/tabify/__tests__/tabify.js @@ -1,8 +1,8 @@ +import './_get_columns'; +import './_buckets'; +import './_table'; +import './_table_group'; +import './_response_writer'; +import './_integration'; describe('Tabify Agg Response', function () { - require('./_get_columns'); - require('./_buckets'); - require('./_table'); - require('./_table_group'); - require('./_response_writer'); - require('./_integration'); }); diff --git a/src/ui/public/agg_response/tabify/_buckets.js b/src/ui/public/agg_response/tabify/_buckets.js index 6fc08cd41e75f..7d05c83a882b1 100644 --- a/src/ui/public/agg_response/tabify/_buckets.js +++ b/src/ui/public/agg_response/tabify/_buckets.js @@ -1,34 +1,32 @@ -define(function (require) { - return function AggResponseBucketsProvider() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function AggResponseBucketsProvider() { - function Buckets(aggResp) { - aggResp = aggResp || false; - this.buckets = aggResp.buckets || []; - this.objectMode = _.isPlainObject(this.buckets); + function Buckets(aggResp) { + aggResp = aggResp || false; + this.buckets = aggResp.buckets || []; + this.objectMode = _.isPlainObject(this.buckets); - if (this.objectMode) { - this._keys = _.keys(this.buckets); - this.length = this._keys.length; - } else { - this.length = this.buckets.length; - } + if (this.objectMode) { + this._keys = _.keys(this.buckets); + this.length = this._keys.length; + } else { + this.length = this.buckets.length; } + } - Buckets.prototype.forEach = function (fn) { - var buckets = this.buckets; + Buckets.prototype.forEach = function (fn) { + let buckets = this.buckets; - if (this.objectMode) { - this._keys.forEach(function (key) { - fn(buckets[key], key); - }); - } else { - buckets.forEach(function (bucket) { - fn(bucket, bucket.key); - }); - } - }; - - return Buckets; + if (this.objectMode) { + this._keys.forEach(function (key) { + fn(buckets[key], key); + }); + } else { + buckets.forEach(function (bucket) { + fn(bucket, bucket.key); + }); + } }; -}); + + return Buckets; +}; diff --git a/src/ui/public/agg_response/tabify/_get_columns.js b/src/ui/public/agg_response/tabify/_get_columns.js index e652cf0f61e02..b9268891cadc3 100644 --- a/src/ui/public/agg_response/tabify/_get_columns.js +++ b/src/ui/public/agg_response/tabify/_get_columns.js @@ -1,51 +1,50 @@ -define(function (require) { - return function GetColumnsProvider(Private) { - var _ = require('lodash'); - var AggConfig = Private(require('ui/Vis/AggConfig')); - - return function getColumns(vis, minimal) { - var aggs = vis.aggs.getResponseAggs(); - - if (minimal == null) minimal = !vis.isHierarchical(); - - if (!vis.aggs.bySchemaGroup.metrics) { - aggs.push(new AggConfig(vis, { - type: 'count', - schema: vis.type.schemas.metrics[0].name - })); - } - - // pick the columns - if (minimal) { - return aggs.map(function (agg) { - return { aggConfig: agg }; - }); - } - - // supposed to be bucket,...metrics,bucket,...metrics - var columns = []; - - // seperate the metrics - var grouped = _.groupBy(aggs, function (agg) { - return agg.schema.group; +import _ from 'lodash'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +export default function GetColumnsProvider(Private) { + let AggConfig = Private(VisAggConfigProvider); + + return function getColumns(vis, minimal) { + let aggs = vis.aggs.getResponseAggs(); + + if (minimal == null) minimal = !vis.isHierarchical(); + + if (!vis.aggs.bySchemaGroup.metrics) { + aggs.push(new AggConfig(vis, { + type: 'count', + schema: vis.type.schemas.metrics[0].name + })); + } + + // pick the columns + if (minimal) { + return aggs.map(function (agg) { + return { aggConfig: agg }; }); + } - if (!grouped.buckets) { - // return just the metrics, in column format - return grouped.metrics.map(function (agg) { - return { aggConfig: agg }; - }); - } - - // return the buckets, and after each place all of the metrics - grouped.buckets.forEach(function (agg, i) { - columns.push({ aggConfig: agg }); - grouped.metrics.forEach(function (metric) { - columns.push({ aggConfig: metric }); - }); + // supposed to be bucket,...metrics,bucket,...metrics + let columns = []; + + // seperate the metrics + let grouped = _.groupBy(aggs, function (agg) { + return agg.schema.group; + }); + + if (!grouped.buckets) { + // return just the metrics, in column format + return grouped.metrics.map(function (agg) { + return { aggConfig: agg }; + }); + } + + // return the buckets, and after each place all of the metrics + grouped.buckets.forEach(function (agg, i) { + columns.push({ aggConfig: agg }); + grouped.metrics.forEach(function (metric) { + columns.push({ aggConfig: metric }); }); + }); - return columns; - }; + return columns; }; -}); +}; diff --git a/src/ui/public/agg_response/tabify/_response_writer.js b/src/ui/public/agg_response/tabify/_response_writer.js index f6da52f7b4229..53f1ee01782ac 100644 --- a/src/ui/public/agg_response/tabify/_response_writer.js +++ b/src/ui/public/agg_response/tabify/_response_writer.js @@ -1,282 +1,283 @@ -define(function (require) { - return function TabbedAggResponseWriterProvider(Private) { - var _ = require('lodash'); - var Table = Private(require('ui/agg_response/tabify/_table')); - var TableGroup = Private(require('ui/agg_response/tabify/_table_group')); - var getColumns = Private(require('ui/agg_response/tabify/_get_columns')); - - var AggConfigResult = require('ui/Vis/AggConfigResult'); - - _.class(SplitAcr).inherits(AggConfigResult); - function SplitAcr(agg, parent, key) { - SplitAcr.Super.call(this, agg, parent, key, key); +import _ from 'lodash'; +import AggConfigResult from 'ui/vis/agg_config_result'; +import AggResponseTabifyTableProvider from 'ui/agg_response/tabify/_table'; +import AggResponseTabifyTableGroupProvider from 'ui/agg_response/tabify/_table_group'; +import AggResponseTabifyGetColumnsProvider from 'ui/agg_response/tabify/_get_columns'; +export default function TabbedAggResponseWriterProvider(Private) { + let Table = Private(AggResponseTabifyTableProvider); + let TableGroup = Private(AggResponseTabifyTableGroupProvider); + let getColumns = Private(AggResponseTabifyGetColumnsProvider); + + + _.class(SplitAcr).inherits(AggConfigResult); + function SplitAcr(agg, parent, key) { + SplitAcr.Super.call(this, agg, parent, key, key); + } + + /** + * Writer class that collects information about an aggregation response and + * produces a table, or a series of tables. + * + * @param {Vis} vis - the vis object to which the aggregation response correlates + */ + function TabbedAggResponseWriter(vis, opts) { + this.vis = vis; + this.opts = opts || {}; + this.rowBuffer = []; + + let visIsHier = vis.isHierarchical(); + + // do the options allow for splitting? we will only split if true and + // tabify calls the split method. + this.canSplit = this.opts.canSplit !== false; + + // should we allow partial rows to be included in the tables? if a + // partial row is found, it is filled with empty strings '' + this.partialRows = this.opts.partialRows == null ? visIsHier : this.opts.partialRows; + + // if true, we will not place metric columns after every bucket + // even if the vis is hierarchical. if false, and the vis is + // hierarchical, then we will display metric columns after + // every bucket col + this.minimalColumns = visIsHier ? !!this.opts.minimalColumns : true; + + // true if we can expect metrics to have been calculated + // for every bucket + this.metricsForAllBuckets = visIsHier; + + // if true, values will be wrapped in aggConfigResult objects which link them + // to their aggConfig and enable the filterbar and tooltip formatters + this.asAggConfigResults = !!this.opts.asAggConfigResults; + + this.columns = getColumns(vis, this.minimalColumns); + this.aggStack = _.pluck(this.columns, 'aggConfig'); + + this.root = new TableGroup(); + this.acrStack = []; + this.splitStack = [this.root]; + } + + /** + * Create a Table of TableGroup object, link it to it's parent (if any), and determine if + * it's the root + * + * @param {boolean} group - is this a TableGroup or just a normal Table + * @param {AggConfig} agg - the aggregation that create this table, only applies to groups + * @param {any} key - the bucketKey that this table relates to + * @return {Table/TableGroup} table - the created table + */ + TabbedAggResponseWriter.prototype._table = function (group, agg, key) { + let Class = (group) ? TableGroup : Table; + let table = new Class(); + let parent = this.splitStack[0]; + + if (group) { + table.aggConfig = agg; + table.key = key; + table.title = (table.fieldFormatter()(key)) + ': ' + agg.makeLabel(); } - /** - * Writer class that collects information about an aggregation response and - * produces a table, or a series of tables. - * - * @param {Vis} vis - the vis object to which the aggregation response correlates - */ - function TabbedAggResponseWriter(vis, opts) { - this.vis = vis; - this.opts = opts || {}; - this.rowBuffer = []; - - var visIsHier = vis.isHierarchical(); - - // do the options allow for splitting? we will only split if true and - // tabify calls the split method. - this.canSplit = this.opts.canSplit !== false; - - // should we allow partial rows to be included in the tables? if a - // partial row is found, it is filled with empty strings '' - this.partialRows = this.opts.partialRows == null ? visIsHier : this.opts.partialRows; - - // if true, we will not place metric columns after every bucket - // even if the vis is hierarchical. if false, and the vis is - // hierarchical, then we will display metric columns after - // every bucket col - this.minimalColumns = visIsHier ? !!this.opts.minimalColumns : true; - - // true if we can expect metrics to have been calculated - // for every bucket - this.metricsForAllBuckets = visIsHier; - - // if true, values will be wrapped in aggConfigResult objects which link them - // to their aggConfig and enable the filterbar and tooltip formatters - this.asAggConfigResults = !!this.opts.asAggConfigResults; - - this.columns = getColumns(vis, this.minimalColumns); - this.aggStack = _.pluck(this.columns, 'aggConfig'); - - this.root = new TableGroup(); - this.acrStack = []; - this.splitStack = [this.root]; + // link the parent and child + table.$parent = parent; + parent.tables.push(table); + + return table; + }; + + /** + * Enter into a split table, called for each bucket of a splitting agg. The new table + * is either created or located using the agg and key arguments, and then the block is + * executed with the table as it's this context. Within this function, you should + * walk into the remaining branches and end up writing some rows to the table. + * + * @param {aggConfig} agg - the aggConfig that created this split + * @param {Buckets} buckets - the buckets produces by the agg + * @param {function} block - a function to execute for each sub bucket + */ + TabbedAggResponseWriter.prototype.split = function (agg, buckets, block) { + let self = this; + + if (!self.canSplit) { + throw new Error('attempted to split when splitting is disabled'); } - /** - * Create a Table of TableGroup object, link it to it's parent (if any), and determine if - * it's the root - * - * @param {boolean} group - is this a TableGroup or just a normal Table - * @param {AggConfig} agg - the aggregation that create this table, only applies to groups - * @param {any} key - the bucketKey that this table relates to - * @return {Table/TableGroup} table - the created table - */ - TabbedAggResponseWriter.prototype._table = function (group, agg, key) { - var Class = (group) ? TableGroup : Table; - var table = new Class(); - var parent = this.splitStack[0]; - - if (group) { - table.aggConfig = agg; - table.key = key; - table.title = (table.fieldFormatter()(key)) + ': ' + agg.makeLabel(); + self._removeAggFromColumns(agg); + + buckets.forEach(function (bucket, key) { + // find the existing split that we should extend + let tableGroup = _.find(self.splitStack[0].tables, { aggConfig: agg, key: key }); + // create the split if it doesn't exist yet + if (!tableGroup) tableGroup = self._table(true, agg, key); + + let splitAcr = false; + if (self.asAggConfigResults) { + splitAcr = self._injectParentSplit(agg, key); } - // link the parent and child - table.$parent = parent; - parent.tables.push(table); - - return table; - }; - - /** - * Enter into a split table, called for each bucket of a splitting agg. The new table - * is either created or located using the agg and key arguments, and then the block is - * executed with the table as it's this context. Within this function, you should - * walk into the remaining branches and end up writing some rows to the table. - * - * @param {aggConfig} agg - the aggConfig that created this split - * @param {Buckets} buckets - the buckets produces by the agg - * @param {function} block - a function to execute for each sub bucket - */ - TabbedAggResponseWriter.prototype.split = function (agg, buckets, block) { - var self = this; - - if (!self.canSplit) { - throw new Error('attempted to split when splitting is disabled'); + // push the split onto the stack so that it will receive written tables + self.splitStack.unshift(tableGroup); + + // call the block + if (_.isFunction(block)) block.call(self, bucket, key); + + // remove the split from the stack + self.splitStack.shift(); + splitAcr && _.pull(self.acrStack, splitAcr); + }); + }; + + TabbedAggResponseWriter.prototype._removeAggFromColumns = function (agg) { + let i = _.findIndex(this.columns, function (col) { + return col.aggConfig === agg; + }); + + // we must have already removed this column + if (i === -1) return; + + this.columns.splice(i, 1); + + if (this.minimalColumns) return; + + // hierarchical vis creats additional columns for each bucket + // we will remove those too + let mCol = this.columns.splice(i, 1).pop(); + let mI = _.findIndex(this.aggStack, function (agg) { + return agg === mCol.aggConfig; + }); + + if (mI > -1) this.aggStack.splice(mI, 1); + }; + + /** + * When a split is found while building the aggConfigResult tree, we + * want to push the split into the tree at another point. Since each + * branch in the tree is a double-linked list we need do some special + * shit to pull this off. + * + * @private + * @param {AggConfig} - The agg which produced the split bucket + * @param {any} - The value which identifies the bucket + * @return {SplitAcr} - the AggConfigResult created for the split bucket + */ + TabbedAggResponseWriter.prototype._injectParentSplit = function (agg, key) { + let oldList = this.acrStack; + let newList = this.acrStack = []; + + // walk from right to left through the old stack + // and move things to the new stack + let injected = false; + + if (!oldList.length) { + injected = new SplitAcr(agg, null, key); + newList.unshift(injected); + return injected; + } + + // walk from right to left, emptying the previous list + while (oldList.length) { + let acr = oldList.pop(); + + // ignore other splits + if (acr instanceof SplitAcr) { + newList.unshift(acr); + continue; } - self._removeAggFromColumns(agg); - - buckets.forEach(function (bucket, key) { - // find the existing split that we should extend - var tableGroup = _.find(self.splitStack[0].tables, { aggConfig: agg, key: key }); - // create the split if it doesn't exist yet - if (!tableGroup) tableGroup = self._table(true, agg, key); - - var splitAcr = false; - if (self.asAggConfigResults) { - splitAcr = self._injectParentSplit(agg, key); - } - - // push the split onto the stack so that it will receive written tables - self.splitStack.unshift(tableGroup); - - // call the block - if (_.isFunction(block)) block.call(self, bucket, key); - - // remove the split from the stack - self.splitStack.shift(); - splitAcr && _.pull(self.acrStack, splitAcr); - }); - }; - - TabbedAggResponseWriter.prototype._removeAggFromColumns = function (agg) { - var i = _.findIndex(this.columns, function (col) { - return col.aggConfig === agg; - }); - - // we must have already removed this column - if (i === -1) return; - - this.columns.splice(i, 1); - - if (this.minimalColumns) return; - - // hierarchical vis creats additional columns for each bucket - // we will remove those too - var mCol = this.columns.splice(i, 1).pop(); - var mI = _.findIndex(this.aggStack, function (agg) { - return agg === mCol.aggConfig; - }); - - if (mI > -1) this.aggStack.splice(mI, 1); - }; - - /** - * When a split is found while building the aggConfigResult tree, we - * want to push the split into the tree at another point. Since each - * branch in the tree is a double-linked list we need do some special - * shit to pull this off. - * - * @private - * @param {AggConfig} - The agg which produced the split bucket - * @param {any} - The value which identifies the bucket - * @return {SplitAcr} - the AggConfigResult created for the split bucket - */ - TabbedAggResponseWriter.prototype._injectParentSplit = function (agg, key) { - var oldList = this.acrStack; - var newList = this.acrStack = []; - - // walk from right to left through the old stack - // and move things to the new stack - var injected = false; - - if (!oldList.length) { - injected = new SplitAcr(agg, null, key); + // inject the split + if (!injected) { + injected = new SplitAcr(agg, newList[0], key); newList.unshift(injected); - return injected; } - // walk from right to left, emptying the previous list - while (oldList.length) { - var acr = oldList.pop(); - - // ignore other splits - if (acr instanceof SplitAcr) { - newList.unshift(acr); - continue; - } - - // inject the split - if (!injected) { - injected = new SplitAcr(agg, newList[0], key); - newList.unshift(injected); - } - - var newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr)); - newList.unshift(newAcr); - - // and replace the acr in the row buffer if its there - var rowI = this.rowBuffer.indexOf(acr); - if (rowI > -1) { - this.rowBuffer[rowI] = newAcr; - } - } + let newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr)); + newList.unshift(newAcr); - return injected; - }; - - /** - * Push a value into the row, then run a block. Once the block is - * complete the value is pulled from the stack. - * - * @param {any} value - the value that should be added to the row - * @param {function} block - the function to run while this value is in the row - * @return {any} - the value that was added - */ - TabbedAggResponseWriter.prototype.cell = function (agg, value, block) { - if (this.asAggConfigResults) { - value = new AggConfigResult(agg, this.acrStack[0], value, value); + // and replace the acr in the row buffer if its there + let rowI = this.rowBuffer.indexOf(acr); + if (rowI > -1) { + this.rowBuffer[rowI] = newAcr; } + } - var staskResult = this.asAggConfigResults && value.type === 'bucket'; + return injected; + }; - this.rowBuffer.push(value); - if (staskResult) this.acrStack.unshift(value); + /** + * Push a value into the row, then run a block. Once the block is + * complete the value is pulled from the stack. + * + * @param {any} value - the value that should be added to the row + * @param {function} block - the function to run while this value is in the row + * @return {any} - the value that was added + */ + TabbedAggResponseWriter.prototype.cell = function (agg, value, block) { + if (this.asAggConfigResults) { + value = new AggConfigResult(agg, this.acrStack[0], value, value); + } - if (_.isFunction(block)) block.call(this); + let staskResult = this.asAggConfigResults && value.type === 'bucket'; - this.rowBuffer.pop(value); - if (staskResult) this.acrStack.shift(); + this.rowBuffer.push(value); + if (staskResult) this.acrStack.unshift(value); - return value; - }; + if (_.isFunction(block)) block.call(this); - /** - * Create a new row by reading the row buffer. This will do nothing if - * the row is incomplete and the vis this data came from is NOT flagged as - * hierarchical. - * - * @param {array} [buffer] - optional buffer to use in place of the stored rowBuffer - * @return {undefined} - */ - TabbedAggResponseWriter.prototype.row = function (buffer) { - var cells = buffer || this.rowBuffer.slice(0); + this.rowBuffer.pop(value); + if (staskResult) this.acrStack.shift(); - if (!this.partialRows && cells.length < this.columns.length) { - return; - } + return value; + }; - var split = this.splitStack[0]; - var table = split.tables[0] || this._table(false); + /** + * Create a new row by reading the row buffer. This will do nothing if + * the row is incomplete and the vis this data came from is NOT flagged as + * hierarchical. + * + * @param {array} [buffer] - optional buffer to use in place of the stored rowBuffer + * @return {undefined} + */ + TabbedAggResponseWriter.prototype.row = function (buffer) { + let cells = buffer || this.rowBuffer.slice(0); + + if (!this.partialRows && cells.length < this.columns.length) { + return; + } - while (cells.length < this.columns.length) cells.push(''); - table.rows.push(cells); - return table; - }; + let split = this.splitStack[0]; + let table = split.tables[0] || this._table(false); - /** - * Get the actual response - * - * @return {object} - the final table-tree - */ - TabbedAggResponseWriter.prototype.response = function () { - var columns = this.columns; + while (cells.length < this.columns.length) cells.push(''); + table.rows.push(cells); + return table; + }; - // give the columns some metadata - columns.map(function (col) { - col.title = col.aggConfig.makeLabel(); - }); + /** + * Get the actual response + * + * @return {object} - the final table-tree + */ + TabbedAggResponseWriter.prototype.response = function () { + let columns = this.columns; - // walk the tree and write the columns to each table - (function step(table, group) { - if (table.tables) table.tables.forEach(step); - else table.columns = columns.slice(0); - }(this.root)); + // give the columns some metadata + columns.map(function (col) { + col.title = col.aggConfig.makeLabel(); + }); - if (this.canSplit) return this.root; + // walk the tree and write the columns to each table + (function step(table, group) { + if (table.tables) table.tables.forEach(step); + else table.columns = columns.slice(0); + }(this.root)); - var table = this.root.tables[0]; - if (!table) return; + if (this.canSplit) return this.root; - delete table.$parent; - return table; - }; + let table = this.root.tables[0]; + if (!table) return; - return TabbedAggResponseWriter; + delete table.$parent; + return table; }; -}); + + return TabbedAggResponseWriter; +}; diff --git a/src/ui/public/agg_response/tabify/_table.js b/src/ui/public/agg_response/tabify/_table.js index 855a72a6b4df2..ac26c29f67372 100644 --- a/src/ui/public/agg_response/tabify/_table.js +++ b/src/ui/public/agg_response/tabify/_table.js @@ -1,40 +1,38 @@ -define(function (require) { - return function TableProvider() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function TableProvider() { - /** - * Simple table class that is used to contain the rows and columns that create - * a table. This is usually found at the root of the response or within a TableGroup - */ - function Table() { - this.columns = null; // written with the first row - this.rows = []; - } + /** + * Simple table class that is used to contain the rows and columns that create + * a table. This is usually found at the root of the response or within a TableGroup + */ + function Table() { + this.columns = null; // written with the first row + this.rows = []; + } - Table.prototype.title = function () { - if (this.$parent) { - return this.$parent.title; - } else { - return ''; - } - }; + Table.prototype.title = function () { + if (this.$parent) { + return this.$parent.title; + } else { + return ''; + } + }; - Table.prototype.aggConfig = function (col) { - if (!col.aggConfig) { - throw new TypeError('Column is missing the aggConfig property'); - } - return col.aggConfig; - }; + Table.prototype.aggConfig = function (col) { + if (!col.aggConfig) { + throw new TypeError('Column is missing the aggConfig property'); + } + return col.aggConfig; + }; - Table.prototype.field = function (col) { - return this.aggConfig(col).field(); - }; + Table.prototype.field = function (col) { + return this.aggConfig(col).field(); + }; - Table.prototype.fieldFormatter = function (col) { - return this.aggConfig(col).fieldFormatter(); - }; + Table.prototype.fieldFormatter = function (col) { + return this.aggConfig(col).fieldFormatter(); + }; - return Table; - }; -}); + return Table; +}; diff --git a/src/ui/public/agg_response/tabify/_table_group.js b/src/ui/public/agg_response/tabify/_table_group.js index 28830e82493ba..5f7eee52887d3 100644 --- a/src/ui/public/agg_response/tabify/_table_group.js +++ b/src/ui/public/agg_response/tabify/_table_group.js @@ -1,26 +1,24 @@ -define(function (require) { - return function TableGroupProvider() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function TableGroupProvider() { - /** - * Simple object that wraps multiple tables. It contains information about the aggConfig - * and bucket that created this group and a list of the tables within it. - */ - function TableGroup() { - this.aggConfig = null; - this.key = null; - this.title = null; - this.tables = []; - } + /** + * Simple object that wraps multiple tables. It contains information about the aggConfig + * and bucket that created this group and a list of the tables within it. + */ + function TableGroup() { + this.aggConfig = null; + this.key = null; + this.title = null; + this.tables = []; + } - TableGroup.prototype.field = function () { - if (this.aggConfig) return this.aggConfig.field(); - }; - - TableGroup.prototype.fieldFormatter = function () { - if (this.aggConfig) return this.aggConfig.fieldFormatter(); - }; + TableGroup.prototype.field = function () { + if (this.aggConfig) return this.aggConfig.field(); + }; - return TableGroup; + TableGroup.prototype.fieldFormatter = function () { + if (this.aggConfig) return this.aggConfig.fieldFormatter(); }; -}); + + return TableGroup; +}; diff --git a/src/ui/public/agg_response/tabify/tabify.js b/src/ui/public/agg_response/tabify/tabify.js index 4caa8ee92a93d..722a1a12b8032 100644 --- a/src/ui/public/agg_response/tabify/tabify.js +++ b/src/ui/public/agg_response/tabify/tabify.js @@ -1,103 +1,104 @@ -define(function (require) { - return function tabifyAggResponseProvider(Private, Notifier) { - var _ = require('lodash'); +import _ from 'lodash'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import AggResponseTabifyResponseWriterProvider from 'ui/agg_response/tabify/_response_writer'; +import AggResponseTabifyBucketsProvider from 'ui/agg_response/tabify/_buckets'; +export default function tabifyAggResponseProvider(Private, Notifier) { - var AggConfig = Private(require('ui/Vis/AggConfig')); - var TabbedAggResponseWriter = Private(require('ui/agg_response/tabify/_response_writer')); - var Buckets = Private(require('ui/agg_response/tabify/_buckets')); - var notify = new Notifier({ location: 'agg_response/tabify'}); + let AggConfig = Private(VisAggConfigProvider); + let TabbedAggResponseWriter = Private(AggResponseTabifyResponseWriterProvider); + let Buckets = Private(AggResponseTabifyBucketsProvider); + let notify = new Notifier({ location: 'agg_response/tabify'}); - function tabifyAggResponse(vis, esResponse, respOpts) { - var write = new TabbedAggResponseWriter(vis, respOpts); + function tabifyAggResponse(vis, esResponse, respOpts) { + let write = new TabbedAggResponseWriter(vis, respOpts); - var topLevelBucket = _.assign({}, esResponse.aggregations, { - doc_count: esResponse.hits.total - }); + let topLevelBucket = _.assign({}, esResponse.aggregations, { + doc_count: esResponse.hits.total + }); - collectBucket(write, topLevelBucket); + collectBucket(write, topLevelBucket); - return write.response(); - } + return write.response(); + } - /** - * read an aggregation from a bucket, which is *might* be found at key (if - * the response came in object form), and will recurse down the aggregation - * tree and will pass the read values to the ResponseWriter. - * - * @param {object} bucket - a bucket from the aggResponse - * @param {undefined|string} key - the key where the bucket was found - * @returns {undefined} - */ - function collectBucket(write, bucket, key) { - var agg = write.aggStack.shift(); + /** + * read an aggregation from a bucket, which is *might* be found at key (if + * the response came in object form), and will recurse down the aggregation + * tree and will pass the read values to the ResponseWriter. + * + * @param {object} bucket - a bucket from the aggResponse + * @param {undefined|string} key - the key where the bucket was found + * @returns {undefined} + */ + function collectBucket(write, bucket, key) { + let agg = write.aggStack.shift(); - switch (agg.schema.group) { - case 'buckets': - var buckets = new Buckets(bucket[agg.id]); - if (buckets.length) { - var splitting = write.canSplit && agg.schema.name === 'split'; - if (splitting) { - write.split(agg, buckets, function forEachBucket(subBucket, key) { - collectBucket(write, subBucket, agg.getKey(subBucket), key); - }); - } else { - buckets.forEach(function (subBucket, key) { - write.cell(agg, agg.getKey(subBucket, key), function () { - collectBucket(write, subBucket, agg.getKey(subBucket, key)); - }); - }); - } - } else if (write.partialRows && write.metricsForAllBuckets && write.minimalColumns) { - // we don't have any buckets, but we do have metrics at this - // level, then pass all the empty buckets and jump back in for - // the metrics. - write.aggStack.unshift(agg); - passEmptyBuckets(write, bucket, key); - write.aggStack.shift(); + switch (agg.schema.group) { + case 'buckets': + let buckets = new Buckets(bucket[agg.id]); + if (buckets.length) { + let splitting = write.canSplit && agg.schema.name === 'split'; + if (splitting) { + write.split(agg, buckets, function forEachBucket(subBucket, key) { + collectBucket(write, subBucket, agg.getKey(subBucket), key); + }); } else { - // we don't have any buckets, and we don't have isHierarchical - // data, so no metrics, just try to write the row + buckets.forEach(function (subBucket, key) { + write.cell(agg, agg.getKey(subBucket, key), function () { + collectBucket(write, subBucket, agg.getKey(subBucket, key)); + }); + }); + } + } else if (write.partialRows && write.metricsForAllBuckets && write.minimalColumns) { + // we don't have any buckets, but we do have metrics at this + // level, then pass all the empty buckets and jump back in for + // the metrics. + write.aggStack.unshift(agg); + passEmptyBuckets(write, bucket, key); + write.aggStack.shift(); + } else { + // we don't have any buckets, and we don't have isHierarchical + // data, so no metrics, just try to write the row + write.row(); + } + break; + case 'metrics': + let value = agg.getValue(bucket); + write.cell(agg, value, function () { + if (!write.aggStack.length) { + // row complete write.row(); + } else { + // process the next agg at this same level + collectBucket(write, bucket, key); } - break; - case 'metrics': - var value = agg.getValue(bucket); - write.cell(agg, value, function () { - if (!write.aggStack.length) { - // row complete - write.row(); - } else { - // process the next agg at this same level - collectBucket(write, bucket, key); - } - }); - break; - } - - write.aggStack.unshift(agg); + }); + break; } - // write empty values for each bucket agg, then write - // the metrics from the initial bucket using collectBucket() - function passEmptyBuckets(write, bucket, key) { - var agg = write.aggStack.shift(); + write.aggStack.unshift(agg); + } - switch (agg.schema.group) { - case 'metrics': - // pass control back to collectBucket() - write.aggStack.unshift(agg); - collectBucket(write, bucket, key); - return; + // write empty values for each bucket agg, then write + // the metrics from the initial bucket using collectBucket() + function passEmptyBuckets(write, bucket, key) { + let agg = write.aggStack.shift(); - case 'buckets': - write.cell(agg, '', function () { - passEmptyBuckets(write, bucket, key); - }); - } + switch (agg.schema.group) { + case 'metrics': + // pass control back to collectBucket() + write.aggStack.unshift(agg); + collectBucket(write, bucket, key); + return; - write.aggStack.unshift(agg); + case 'buckets': + write.cell(agg, '', function () { + passEmptyBuckets(write, bucket, key); + }); } - return notify.timed('tabify agg response', tabifyAggResponse); - }; -}); + write.aggStack.unshift(agg); + } + + return notify.timed('tabify agg response', tabifyAggResponse); +}; diff --git a/src/ui/public/agg_table/__tests__/_group.js b/src/ui/public/agg_table/__tests__/_group.js index 945a2f274dd6b..88c7cb92092ed 100644 --- a/src/ui/public/agg_table/__tests__/_group.js +++ b/src/ui/public/agg_table/__tests__/_group.js @@ -1,27 +1,30 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import fixtures from 'fixtures/fake_hierarchical_data'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import VisProvider from 'ui/vis'; describe('AggTableGroup Directive', function () { - var _ = require('lodash'); - var $ = require('jquery'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - var fixtures = require('fixtures/fake_hierarchical_data'); - - var $rootScope; - var $compile; - var tabifyAggResponse; - var Vis; - var indexPattern; + + let $rootScope; + let $compile; + let tabifyAggResponse; + let Vis; + let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector, Private) { - tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - Vis = Private(require('ui/Vis')); + tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + Vis = Private(VisProvider); $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); })); - var $scope; + let $scope; beforeEach(function () { $scope = $rootScope.$new(); }); @@ -31,9 +34,13 @@ describe('AggTableGroup Directive', function () { it('renders a simple split response properly', function () { - var vis = new Vis(indexPattern, 'table'); + let vis = new Vis(indexPattern, 'table'); $scope.group = tabifyAggResponse(vis, fixtures.metricOnly); - var $el = $(''); + $scope.sort = { + columnIndex: null, + direction: null + }; + let $el = $(''); $compile($el)($scope); $scope.$digest(); @@ -43,7 +50,7 @@ describe('AggTableGroup Directive', function () { }); it('renders nothing if the table list is empty', function () { - var $el = $(''); + let $el = $(''); $scope.group = { tables: [] @@ -52,12 +59,12 @@ describe('AggTableGroup Directive', function () { $compile($el)($scope); $scope.$digest(); - var $subTables = $el.find('kbn-agg-table'); + let $subTables = $el.find('kbn-agg-table'); expect($subTables.size()).to.be(0); }); it('renders a complex response properly', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -70,15 +77,15 @@ describe('AggTableGroup Directive', function () { agg.id = 'agg_' + (i + 1); }); - var group = $scope.group = tabifyAggResponse(vis, fixtures.threeTermBuckets); - var $el = $(''); + let group = $scope.group = tabifyAggResponse(vis, fixtures.threeTermBuckets); + let $el = $(''); $compile($el)($scope); $scope.$digest(); - var $subTables = $el.find('kbn-agg-table'); + let $subTables = $el.find('kbn-agg-table'); expect($subTables.size()).to.be(3); - var $subTableHeaders = $el.find('.agg-table-group-header'); + let $subTableHeaders = $el.find('.agg-table-group-header'); expect($subTableHeaders.size()).to.be(3); $subTableHeaders.each(function (i) { diff --git a/src/ui/public/agg_table/__tests__/_table.js b/src/ui/public/agg_table/__tests__/_table.js index 0347f05986496..062bd6ccfb37f 100644 --- a/src/ui/public/agg_table/__tests__/_table.js +++ b/src/ui/public/agg_table/__tests__/_table.js @@ -1,28 +1,31 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import fixtures from 'fixtures/fake_hierarchical_data'; +import sinon from 'auto-release-sinon'; +import AggResponseTabifyTabifyProvider from 'ui/agg_response/tabify/tabify'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import VisProvider from 'ui/vis'; describe('AggTable Directive', function () { - var _ = require('lodash'); - var $ = require('jquery'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - var fixtures = require('fixtures/fake_hierarchical_data'); - var sinon = require('auto-release-sinon'); - - var $rootScope; - var $compile; - var tabifyAggResponse; - var Vis; - var indexPattern; + + let $rootScope; + let $compile; + let tabifyAggResponse; + let Vis; + let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector, Private) { - tabifyAggResponse = Private(require('ui/agg_response/tabify/tabify')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - Vis = Private(require('ui/Vis')); + tabifyAggResponse = Private(AggResponseTabifyTabifyProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + Vis = Private(VisProvider); $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); })); - var $scope; + let $scope; beforeEach(function () { $scope = $rootScope.$new(); }); @@ -32,10 +35,10 @@ describe('AggTable Directive', function () { it('renders a simple response properly', function () { - var vis = new Vis(indexPattern, 'table'); + let vis = new Vis(indexPattern, 'table'); $scope.table = tabifyAggResponse(vis, fixtures.metricOnly, { canSplit: false }); - var $el = $compile('')($scope); + let $el = $compile('')($scope); $scope.$digest(); expect($el.find('tbody').size()).to.be(1); @@ -45,14 +48,14 @@ describe('AggTable Directive', function () { it('renders nothing if the table is empty', function () { $scope.table = null; - var $el = $compile('')($scope); + let $el = $compile('')($scope); $scope.$digest(); expect($el.find('tbody').size()).to.be(0); }); it('renders a complex response properly', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, @@ -66,27 +69,27 @@ describe('AggTable Directive', function () { }); $scope.table = tabifyAggResponse(vis, fixtures.threeTermBuckets, { canSplit: false }); - var $el = $(''); + let $el = $(''); $compile($el)($scope); $scope.$digest(); expect($el.find('tbody').size()).to.be(1); - var $rows = $el.find('tbody tr'); + let $rows = $el.find('tbody tr'); expect($rows.size()).to.be.greaterThan(0); function validBytes(str) { expect(str).to.match(/^\d+$/); - var bytesAsNum = _.parseInt(str); + let bytesAsNum = _.parseInt(str); expect(bytesAsNum === 0 || bytesAsNum > 1000).to.be.ok(); } $rows.each(function (i) { // 6 cells in every row - var $cells = $(this).find('td'); + let $cells = $(this).find('td'); expect($cells.size()).to.be(6); - var txts = $cells.map(function () { + let txts = $cells.map(function () { return $(this).text().trim(); }); @@ -106,11 +109,11 @@ describe('AggTable Directive', function () { describe('aggTable.toCsv()', function () { it('escapes and formats the rows and columns properly', function () { - var $el = $compile('')($scope); + let $el = $compile('')($scope); $scope.$digest(); - var $tableScope = $el.isolateScope(); - var aggTable = $tableScope.aggTable; + let $tableScope = $el.isolateScope(); + let aggTable = $tableScope.aggTable; $tableScope.table = { columns: [ @@ -131,7 +134,7 @@ describe('AggTable Directive', function () { }); describe('aggTable.exportAsCsv()', function () { - var origBlob; + let origBlob; function FakeBlob(slices, opts) { this.slices = slices; this.opts = opts; @@ -147,13 +150,13 @@ describe('AggTable Directive', function () { }); it('calls _saveAs properly', function () { - var $el = $compile('')($scope); + let $el = $compile('')($scope); $scope.$digest(); - var $tableScope = $el.isolateScope(); - var aggTable = $tableScope.aggTable; + let $tableScope = $el.isolateScope(); + let aggTable = $tableScope.aggTable; - var saveAs = sinon.stub(aggTable, '_saveAs'); + let saveAs = sinon.stub(aggTable, '_saveAs'); $tableScope.table = { columns: [ { title: 'one' }, @@ -169,7 +172,7 @@ describe('AggTable Directive', function () { aggTable.exportAsCsv(); expect(saveAs.callCount).to.be(1); - var call = saveAs.getCall(0); + let call = saveAs.getCall(0); expect(call.args[0]).to.be.a(FakeBlob); expect(call.args[0].slices).to.eql([ 'one,two,"with double-quotes("")"' + '\r\n' + @@ -180,5 +183,22 @@ describe('AggTable Directive', function () { }); expect(call.args[1]).to.be('somefilename.csv'); }); + + it('should use the export-title attribute', function () { + let expected = 'export file name'; + let $el = $compile(``)($scope); + $scope.$digest(); + + let $tableScope = $el.isolateScope(); + let aggTable = $tableScope.aggTable; + $tableScope.table = { + columns: [], + rows: [] + }; + $tableScope.exportTitle = expected; + $scope.$digest(); + + expect(aggTable.csv.filename).to.equal(`${expected}.csv`); + }); }); }); diff --git a/src/ui/public/agg_table/__tests__/index.js b/src/ui/public/agg_table/__tests__/index.js index 280fc13fb888b..c9daeb733fd1c 100644 --- a/src/ui/public/agg_table/__tests__/index.js +++ b/src/ui/public/agg_table/__tests__/index.js @@ -1,4 +1,4 @@ +import './_group'; +import './_table'; describe('AggTable Component', function () { - require('./_group'); - require('./_table'); }); diff --git a/src/ui/public/agg_table/agg_table.html b/src/ui/public/agg_table/agg_table.html index 419bdca7f1830..6e1a940fc0c4f 100644 --- a/src/ui/public/agg_table/agg_table.html +++ b/src/ui/public/agg_table/agg_table.html @@ -2,7 +2,8 @@ ng-if="rows.length" rows="rows" columns="formattedColumns" - per-page="perPage"> + per-page="perPage" + sort="sort">
    Export:   diff --git a/src/ui/public/agg_table/agg_table.js b/src/ui/public/agg_table/agg_table.js index 75344384b4dfd..1165de1244be9 100644 --- a/src/ui/public/agg_table/agg_table.js +++ b/src/ui/public/agg_table/agg_table.js @@ -1,100 +1,101 @@ -define(function (require) { - require('ui/paginated_table'); - require('ui/compile_recursive_directive'); - require('ui/agg_table/agg_table.less'); +import 'ui/paginated_table'; +import 'ui/compile_recursive_directive'; +import 'ui/agg_table/agg_table.less'; +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import aggTableTemplate from 'ui/agg_table/agg_table.html'; - require('ui/modules') - .get('kibana') - .directive('kbnAggTable', function ($filter, config, Private, compileRecursiveDirective) { - var _ = require('lodash'); +uiModules +.get('kibana') +.directive('kbnAggTable', function ($filter, config, Private, compileRecursiveDirective) { - return { - restrict: 'E', - template: require('ui/agg_table/agg_table.html'), - scope: { - table: '=', - perPage: '=?' - }, - controllerAs: 'aggTable', - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return compileRecursiveDirective.compile($el); - }, - controller: function ($scope) { - var self = this; + return { + restrict: 'E', + template: aggTableTemplate, + scope: { + table: '=', + perPage: '=?', + sort: '=?', + exportTitle: '=?' + }, + controllerAs: 'aggTable', + compile: function ($el) { + // Use the compile function from the RecursionHelper, + // And return the linking function(s) which it returns + return compileRecursiveDirective.compile($el); + }, + controller: function ($scope) { + let self = this; - self.sort = null; - self._saveAs = require('@spalger/filesaver').saveAs; - self.csv = { - separator: config.get('csv:separator'), - quoteValues: config.get('csv:quoteValues') - }; + self._saveAs = require('@spalger/filesaver').saveAs; + self.csv = { + separator: config.get('csv:separator'), + quoteValues: config.get('csv:quoteValues') + }; - self.exportAsCsv = function (formatted) { - var csv = new Blob([self.toCsv(formatted)], { type: 'text/plain' }); - self._saveAs(csv, self.csv.filename); - }; + self.exportAsCsv = function (formatted) { + let csv = new Blob([self.toCsv(formatted)], { type: 'text/plain' }); + self._saveAs(csv, self.csv.filename); + }; - self.toCsv = function (formatted) { - var rows = $scope.table.rows; - var columns = formatted ? $scope.formattedColumns : $scope.table.columns; - var nonAlphaNumRE = /[^a-zA-Z0-9]/; - var allDoubleQuoteRE = /"/g; + self.toCsv = function (formatted) { + let rows = $scope.table.rows; + let columns = formatted ? $scope.formattedColumns : $scope.table.columns; + let nonAlphaNumRE = /[^a-zA-Z0-9]/; + let allDoubleQuoteRE = /"/g; - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; + function escape(val) { + if (!formatted && _.isObject(val)) val = val.valueOf(); + val = String(val); + if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; } + return val; + } - // escape each cell in each row - var csvRows = rows.map(function (row) { - return row.map(escape); - }); + // escape each cell in each row + let csvRows = rows.map(function (row) { + return row.map(escape); + }); - // add the columns to the rows - csvRows.unshift(columns.map(function (col) { - return escape(col.title); - })); + // add the columns to the rows + csvRows.unshift(columns.map(function (col) { + return escape(col.title); + })); - return csvRows.map(function (row) { - return row.join(self.csv.separator) + '\r\n'; - }).join(''); - }; + return csvRows.map(function (row) { + return row.join(self.csv.separator) + '\r\n'; + }).join(''); + }; - $scope.$watch('table', function () { - var table = $scope.table; + $scope.$watch('table', function () { + let table = $scope.table; - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - return; - } + if (!table) { + $scope.rows = null; + $scope.formattedColumns = null; + return; + } - 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(); - var formattedColumn = { - title: col.title, - filterable: field && field.filterable && agg.schema.group === 'buckets' - }; + self.csv.filename = ($scope.exportTitle || table.title() || 'table') + '.csv'; + $scope.rows = table.rows; + $scope.formattedColumns = table.columns.map(function (col, i) { + let agg = $scope.table.aggConfig(col); + let field = agg.field(); + let formattedColumn = { + title: col.title, + filterable: field && field.filterable && agg.schema.group === 'buckets' + }; - var last = i === (table.columns.length - 1); + let last = i === (table.columns.length - 1); - if (last || (agg.schema.group === 'metrics')) { - formattedColumn.class = 'visualize-table-right'; - } + if (last || (agg.schema.group === 'metrics')) { + formattedColumn.class = 'visualize-table-right'; + } - return formattedColumn; - }); + return formattedColumn; }); - } - }; - }); + }); + } + }; }); diff --git a/src/ui/public/agg_table/agg_table_group.html b/src/ui/public/agg_table/agg_table_group.html index 69c369d75331d..5a8defc96d26b 100644 --- a/src/ui/public/agg_table/agg_table_group.html +++ b/src/ui/public/agg_table/agg_table_group.html @@ -9,8 +9,14 @@ - - + + + @@ -27,8 +33,14 @@ - - + + + diff --git a/src/ui/public/agg_table/agg_table_group.js b/src/ui/public/agg_table/agg_table_group.js index 8d331f734f425..cfbf903299c9b 100644 --- a/src/ui/public/agg_table/agg_table_group.js +++ b/src/ui/public/agg_table/agg_table_group.js @@ -1,38 +1,40 @@ -define(function (require) { - require('ui/compile_recursive_directive'); - require('ui/agg_table'); +import 'ui/compile_recursive_directive'; +import 'ui/agg_table'; +import uiModules from 'ui/modules'; +import aggTableGroupTemplate from 'ui/agg_table/agg_table_group.html'; - require('ui/modules') - .get('kibana') - .directive('kbnAggTableGroup', function (compileRecursiveDirective) { - return { - restrict: 'E', - template: require('ui/agg_table/agg_table_group.html'), - scope: { - group: '=', - perPage: '=?' - }, - compile: function ($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return compileRecursiveDirective.compile($el, { - post: function ($scope) { - $scope.$watch('group', function (group) { - // clear the previous "state" - $scope.rows = $scope.columns = false; +uiModules +.get('kibana') +.directive('kbnAggTableGroup', function (compileRecursiveDirective) { + return { + restrict: 'E', + template: aggTableGroupTemplate, + scope: { + group: '=', + perPage: '=?', + sort: '=?', + exportTitle: '=?' + }, + compile: function ($el) { + // Use the compile function from the RecursionHelper, + // And return the linking function(s) which it returns + return compileRecursiveDirective.compile($el, { + post: function ($scope) { + $scope.$watch('group', function (group) { + // clear the previous "state" + $scope.rows = $scope.columns = false; - if (!group || !group.tables.length) return; + if (!group || !group.tables.length) return; - var firstTable = group.tables[0]; - var params = firstTable.aggConfig && firstTable.aggConfig.params; - // render groups that have Table children as if they were rows, because itteration is cleaner - var childLayout = (params && !params.row) ? 'columns' : 'rows'; + let firstTable = group.tables[0]; + let params = firstTable.aggConfig && firstTable.aggConfig.params; + // render groups that have Table children as if they were rows, because iteration is cleaner + let childLayout = (params && !params.row) ? 'columns' : 'rows'; - $scope[childLayout] = group.tables; - }); - } - }); - } - }; - }); + $scope[childLayout] = group.tables; + }); + } + }); + } + }; }); diff --git a/src/ui/public/agg_types/AggParams.js b/src/ui/public/agg_types/AggParams.js deleted file mode 100644 index 503335d9c567e..0000000000000 --- a/src/ui/public/agg_types/AggParams.js +++ /dev/null @@ -1,73 +0,0 @@ -define(function (require) { - return function AggParamsFactory(Private) { - require('ui/filters/label'); - - var _ = require('lodash'); - var IndexedArray = require('ui/IndexedArray'); - - var paramTypeMap = { - field: Private(require('ui/agg_types/param_types/field')), - optioned: Private(require('ui/agg_types/param_types/optioned')), - regex: Private(require('ui/agg_types/param_types/regex')), - string: Private(require('ui/agg_types/param_types/string')), - json: Private(require('ui/agg_types/param_types/raw_json')), - _default: Private(require('ui/agg_types/param_types/base')) - }; - - /** - * Wraps a list of {{#crossLink "AggParam"}}{{/crossLink}} objects; owned by an {{#crossLink "AggType"}}{{/crossLink}} - * - * used to create: - * - `FieldAggParam` – When the config has `name: "field"` - * - `*AggParam` – When the type matches something in the map above - * - `BaseAggParam` – All other params - * - * @class AggParams - * @constructor - * @extends IndexedArray - * @param {object[]} params - array of params that get new-ed up as AggParam objects as descibed above - */ - _.class(AggParams).inherits(IndexedArray); - function AggParams(params) { - AggParams.Super.call(this, { - index: ['name'], - initialSet: params.map(function (config) { - var type = config.name === 'field' ? config.name : config.type; - var Class = paramTypeMap[type] || paramTypeMap._default; - return new Class(config); - }) - }); - } - - /** - * Reads an aggConfigs - * - * @method write - * @param {AggConfig} aggConfig - * the AggConfig object who's type owns these aggParams and contains the param values for our param defs - * @param {object} [locals] - * an array of locals that will be available to the write function (can be used to enhance - * the quality of things like date_histogram's "auto" interval) - * @return {object} output - * output of the write calls, reduced into a single object. A `params: {}` property is exposed on the - * output object which is used to create the agg dsl for the search request. All other properties - * are dependent on the AggParam#write methods which should be studied for each AggType. - */ - AggParams.prototype.write = function (aggConfig, locals) { - var output = { params: {} }; - locals = locals || {}; - - this.forEach(function (param) { - if (param.write) { - param.write(aggConfig, output, locals); - } else { - output.params[param.name] = aggConfig.params[param.name]; - } - }); - - return output; - }; - - return AggParams; - }; -}); diff --git a/src/ui/public/agg_types/AggType.js b/src/ui/public/agg_types/AggType.js deleted file mode 100644 index 85231ddb3a85a..0000000000000 --- a/src/ui/public/agg_types/AggType.js +++ /dev/null @@ -1,141 +0,0 @@ -define(function (require) { - return function AggTypeFactory(Private) { - var _ = require('lodash'); - var AggParams = Private(require('ui/agg_types/AggParams')); - var fieldFormats = Private(require('ui/registry/field_formats')); - - /** - * Generic AggType Constructor - * - * Used to create the values exposed by the agg_types module. - * - * @class AggType - * @private - * @param {object} config - used to set the properties of the AggType - */ - function AggType(config) { - - /** - * the unique, unchanging, name that we have assigned this aggType - * - * @property name - * @type {string} - */ - this.name = config.name; - - /** - * the name of the elasticsearch aggregation that this aggType represents. Usually just this.name - * - * @property name - * @type {string} - */ - this.dslName = config.dslName || config.name; - - /** - * the user friendly name that will be shown in the ui for this aggType - * - * @property title - * @type {string} - */ - this.title = config.title; - - /** - * a function that will be called when this aggType is assigned to - * an aggConfig, and that aggConfig is being rendered (in a form, chart, etc.). - * - * @method makeLabel - * @param {AggConfig} aggConfig - an agg config of this type - * @returns {string} - label that can be used in the ui to descripe the aggConfig - */ - this.makeLabel = config.makeLabel || _.constant(this.name); - - /** - * Describes if this aggType creates data that is ordered, and if that ordered data - * is some sort of time series. - * - * If the aggType does not create ordered data, set this to something "falsey". - * - * If this does create orderedData, then the value should be an object. - * - * If the orderdata is some sort of time series, `this.ordered` should be an object - * with the property `date: true` - * - * @property ordered - * @type {object|undefined} - */ - this.ordered = config.ordered; - - /** - * Flag that prevents this aggregation from being included in the dsl. This is only - * used by the count aggregation (currently) since it doesn't really exist and it's output - * is available on every bucket. - * - * @type {Boolean} - */ - this.hasNoDsl = !!config.hasNoDsl; - - /** - * The method to create a filter representation of the bucket - * @param {object} aggConfig The instance of the aggConfig - * @param {mixed} key The key for the bucket - * @returns {object} The filter - */ - this.createFilter = config.createFilter; - - /** - * An instance of {{#crossLink "AggParams"}}{{/crossLink}}. - * - * @property params - * @type {AggParams} - */ - this.params = config.params || []; - if (!(this.params instanceof AggParams)) { - // always append the raw JSON param - this.params.push({ - name: 'json', - type: 'json', - advanced: true - }); - - this.params = new AggParams(this.params); - } - - /** - * Designed for multi-value metric aggs, this method can return a - * set of AggConfigs that should replace this aggConfig in result sets - * that walk the AggConfig set. - * - * @method getResponseAggs - * @returns {array[AggConfig]|undefined} - an array of aggConfig objects - * that should replace this one, - * or undefined - */ - this.getResponseAggs = config.getResponseAggs || _.noop; - - /** - * A function that will be called each time an aggConfig of this type - * is created, giving the agg type a chance to modify the agg config - */ - this.decorateAggConfig = config.decorateAggConfig || null; - - if (config.getFormat) { - this.getFormat = config.getFormat; - } - } - - /** - * 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} - */ - AggType.prototype.getFormat = function (agg) { - var field = agg.field(); - return field ? field.format : fieldFormats.getDefaultInstance('string'); - }; - - return AggType; - }; -}); diff --git a/src/ui/public/agg_types/__tests__/AggParamWriter.js b/src/ui/public/agg_types/__tests__/AggParamWriter.js deleted file mode 100644 index b37ff90795d57..0000000000000 --- a/src/ui/public/agg_types/__tests__/AggParamWriter.js +++ /dev/null @@ -1,104 +0,0 @@ -module.exports = function AggParamWriterHelper(Private) { - var _ = require('lodash'); - var Vis = Private(require('ui/Vis')); - var aggTypes = Private(require('ui/agg_types/index')); - var visTypes = Private(require('ui/registry/vis_types')); - var stubbedLogstashIndexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - - /** - * Helper object for writing aggParams. Specify an aggType and it will find a vis & schema, and - * wire up the supporting objects required to feed in parameters, and get #write() output. - * - * Use cases: - * - Verify that the interval parameter of the histogram visualization casts its input to a number - * ```js - * it('casts to a number', function () { - * var writer = new AggParamWriter({ aggType: 'histogram' }); - * var output = writer.write({ interval : '100/10' }); - * expect(output.params.interval).to.be.a('number'); - * expect(output.params.interval).to.be(100); - * }); - * ``` - * - * @class AggParamWriter - * @param {object} opts - describe the properties of this paramWriter - * @param {string} opts.aggType - the name of the aggType we want to test. ('histogram', 'filter', etc.) - */ - function AggParamWriter(opts) { - var self = this; - - self.aggType = opts.aggType; - if (_.isString(self.aggType)) { - self.aggType = aggTypes.byName[self.aggType]; - } - - // not configurable right now, but totally required - self.indexPattern = stubbedLogstashIndexPattern; - - // the vis type we will use to write the aggParams - self.visType = null; - - // the schema that the aggType satisfies - self.visAggSchema = null; - - // find a suitable vis type and schema - _.find(visTypes, function (visType) { - var schema = _.find(visType.schemas.all, function (schema) { - // type, type, type, type, type... :( - return schema.group === self.aggType.type; - }); - - if (schema) { - self.visType = visType; - self.visAggSchema = schema; - return true; - } - }); - - if (!self.aggType || !self.visType || !self.visAggSchema) { - throw new Error('unable to find a usable visType and schema for the ' + opts.aggType + ' agg type'); - } - - self.vis = new Vis(self.indexPattern, { - type: self.visType - }); - } - - AggParamWriter.prototype.write = function (paramValues) { - var self = this; - paramValues = _.clone(paramValues); - - if (self.aggType.params.byName.field && !paramValues.field) { - // pick a field rather than force a field to be specified everywhere - if (self.aggType.type === 'metrics') { - paramValues.field = _.sample(self.indexPattern.fields.byType.number); - } else { - paramValues.field = _.sample(self.indexPattern.fields.byType.string); - } - } - - self.vis.setState({ - type: self.vis.type.name, - aggs: [{ - type: self.aggType, - schema: self.visAggSchema, - params: paramValues - }] - }); - - var aggConfig = _.find(self.vis.aggs, function (aggConfig) { - return aggConfig.type === self.aggType; - }); - - aggConfig.type.params.forEach(function (param) { - if (param.onRequest) { - param.onRequest(aggConfig); - } - }); - - return aggConfig.type.params.write(aggConfig); - }; - - return AggParamWriter; - -}; diff --git a/src/ui/public/agg_types/__tests__/AggParams.js b/src/ui/public/agg_types/__tests__/AggParams.js deleted file mode 100644 index 0d0c839fb9aef..0000000000000 --- a/src/ui/public/agg_types/__tests__/AggParams.js +++ /dev/null @@ -1,102 +0,0 @@ -describe('AggParams class', function () { - var _ = require('lodash'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - - var AggParams; - var BaseAggParam; - var FieldAggParam; - var OptionedAggParam; - var RegexAggParam; - - beforeEach(ngMock.module('kibana')); - // stub out the param classes before we get the AggParams - beforeEach(ngMock.inject(require('./utils/_stub_agg_params'))); - // fetch out deps - beforeEach(ngMock.inject(function (Private) { - AggParams = Private(require('ui/agg_types/AggParams')); - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - FieldAggParam = Private(require('ui/agg_types/param_types/field')); - OptionedAggParam = Private(require('ui/agg_types/param_types/optioned')); - RegexAggParam = Private(require('ui/agg_types/param_types/regex')); - })); - - describe('constructor args', function () { - it('accepts an array of param defs', function () { - var params = [ - { name: 'one' }, - { name: 'two' } - ]; - var aggParams = new AggParams(params); - - expect(aggParams).to.have.length(params.length); - expect(aggParams).to.be.an(Array); - expect(aggParams.byName).to.have.keys(['one', 'two']); - }); - }); - - describe('AggParam creation', function () { - it('Uses the FieldAggParam class for params with the name "field"', function () { - var params = [ - { name: 'field' } - ]; - var aggParams = new AggParams(params); - - expect(aggParams).to.have.length(params.length); - expect(aggParams[0]).to.be.a(FieldAggParam); - }); - - it('Uses the OptionedAggParam class for params of type "optioned"', function () { - var params = [ - { - name: 'interval', - type: 'optioned' - } - ]; - var aggParams = new AggParams(params); - - expect(aggParams).to.have.length(params.length); - expect(aggParams[0]).to.be.a(OptionedAggParam); - }); - - it('Uses the RegexAggParam class for params of type "regex"', function () { - var params = [ - { - name: 'exclude', - type: 'regex' - } - ]; - var aggParams = new AggParams(params); - - expect(aggParams).to.have.length(params.length); - expect(aggParams[0]).to.be.a(RegexAggParam); - }); - - it('Always converts the params to a BaseAggParam', function () { - var params = [ - { - name: 'height', - editor: 'high' - }, - { - name: 'weight', - editor: 'big' - }, - { - name: 'waist', - editor: 'small' - } - ]; - var aggParams = new AggParams(params); - - expect(BaseAggParam).to.have.property('callCount', params.length); - expect(FieldAggParam).to.have.property('callCount', 0); - expect(OptionedAggParam).to.have.property('callCount', 0); - - expect(aggParams).to.have.length(params.length); - aggParams.forEach(function (aggParam) { - expect(aggParam).to.be.a(BaseAggParam); - }); - }); - }); -}); diff --git a/src/ui/public/agg_types/__tests__/AggType.js b/src/ui/public/agg_types/__tests__/AggType.js deleted file mode 100644 index 2f3e37f317977..0000000000000 --- a/src/ui/public/agg_types/__tests__/AggType.js +++ /dev/null @@ -1,174 +0,0 @@ -describe('AggType Class', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var sinon = require('auto-release-sinon'); - var AggType; - var AggParams; - var AggConfig; - var indexPattern; - var fieldFormat; - var Vis; - - require('ui/private'); - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - var AggParamsPM = require('ui/agg_types/AggParams'); - AggParams = sinon.spy(Private(AggParamsPM)); - Private.stub(AggParamsPM, AggParams); - - Vis = Private(require('ui/Vis')); - fieldFormat = Private(require('ui/registry/field_formats')); - AggType = Private(require('ui/agg_types/AggType')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - })); - - describe('constructor', function () { - - it('requires a config object as it\'s first param', function () { - expect(function () { - new AggType(null); - }).to.throwError(); - }); - - describe('application of config properties', function () { - var copiedConfigProps = [ - 'name', - 'title', - 'makeLabel', - 'ordered' - ]; - - describe('"' + copiedConfigProps.join('", "') + '"', function () { - it('assigns the config value to itself', function () { - var config = _.transform(copiedConfigProps, function (config, prop) { - config[prop] = {}; - }, {}); - - var aggType = new AggType(config); - - copiedConfigProps.forEach(function (prop) { - expect(aggType[prop]).to.be(config[prop]); - }); - }); - }); - - describe('makeLabel', function () { - it('makes a function when the makeLabel config is not specified', function () { - var someGetter = function () {}; - - var aggType = new AggType({ - makeLabel: someGetter - }); - - expect(aggType.makeLabel).to.be(someGetter); - - aggType = new AggType({ - name: 'pizza' - }); - - expect(aggType.makeLabel).to.be.a('function'); - expect(aggType.makeLabel()).to.be('pizza'); - }); - }); - - describe('getFormat', function () { - it('returns the formatter for the aggConfig', function () { - var aggType = new AggType({}); - - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - - expect(aggType.getFormat(aggConfig)).to.be(fieldFormat.getDefaultInstance('date')); - - vis = new Vis(indexPattern, { - type: 'metric', - aggs: [ - { - type: 'count', - schema: 'metric' - } - ] - }); - aggConfig = vis.aggs.byTypeName.count[0]; - - expect(aggType.getFormat(aggConfig)).to.be(fieldFormat.getDefaultInstance('string')); - }); - - it('can be overridden via config', function () { - var someGetter = function () {}; - - var aggType = new AggType({ - getFormat: someGetter - }); - - expect(aggType.getFormat).to.be(someGetter); - }); - }); - - describe('params', function () { - beforeEach(function () { - AggParams.reset(); - }); - - it('defaults to AggParams object with JSON param', function () { - var aggType = new AggType({ - name: 'smart agg' - }); - - expect(aggType.params).to.be.an(AggParams); - expect(aggType.params.length).to.be(1); - expect(aggType.params[0].name).to.be('json'); - }); - - it('passes the params arg directly to the AggParams constructor', function () { - var params = [ - {name: 'one'}, - {name: 'two'} - ]; - var paramLength = params.length + 1; // json is always appended - - var aggType = new AggType({ - name: 'bucketeer', - params: params - }); - - expect(aggType.params).to.be.an(AggParams); - expect(aggType.params.length).to.be(paramLength); - expect(AggParams.callCount).to.be(1); - expect(AggParams.firstCall.args[0]).to.be(params); - }); - }); - - describe('getResponseAggs', function () { - it('copies the value', function () { - var football = {}; - var aggType = new AggType({ - getResponseAggs: football - }); - - expect(aggType.getResponseAggs).to.be(football); - }); - - it('defaults to _.noop', function () { - var aggType = new AggType({}); - - expect(aggType.getResponseAggs).to.be(_.noop); - }); - }); - }); - - }); - -}); diff --git a/src/ui/public/agg_types/__tests__/agg_param_writer.js b/src/ui/public/agg_types/__tests__/agg_param_writer.js new file mode 100644 index 0000000000000..58d113f2e6c5c --- /dev/null +++ b/src/ui/public/agg_types/__tests__/agg_param_writer.js @@ -0,0 +1,108 @@ +import _ from 'lodash'; +import VisProvider from 'ui/vis'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import RegistryVisTypesProvider from 'ui/registry/vis_types'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +module.exports = function AggParamWriterHelper(Private) { + let Vis = Private(VisProvider); + let aggTypes = Private(AggTypesIndexProvider); + let visTypes = Private(RegistryVisTypesProvider); + let stubbedLogstashIndexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + + /** + * Helper object for writing aggParams. Specify an aggType and it will find a vis & schema, and + * wire up the supporting objects required to feed in parameters, and get #write() output. + * + * Use cases: + * - Verify that the interval parameter of the histogram visualization casts its input to a number + * ```js + * it('casts to a number', function () { + * let writer = new AggParamWriter({ aggType: 'histogram' }); + * let output = writer.write({ interval : '100/10' }); + * expect(output.params.interval).to.be.a('number'); + * expect(output.params.interval).to.be(100); + * }); + * ``` + * + * @class AggParamWriter + * @param {object} opts - describe the properties of this paramWriter + * @param {string} opts.aggType - the name of the aggType we want to test. ('histogram', 'filter', etc.) + */ + function AggParamWriter(opts) { + let self = this; + + self.aggType = opts.aggType; + if (_.isString(self.aggType)) { + self.aggType = aggTypes.byName[self.aggType]; + } + + // not configurable right now, but totally required + self.indexPattern = stubbedLogstashIndexPattern; + + // the vis type we will use to write the aggParams + self.visType = null; + + // the schema that the aggType satisfies + self.visAggSchema = null; + + // find a suitable vis type and schema + _.find(visTypes, function (visType) { + let schema = _.find(visType.schemas.all, function (schema) { + // type, type, type, type, type... :( + return schema.group === self.aggType.type; + }); + + if (schema) { + self.visType = visType; + self.visAggSchema = schema; + return true; + } + }); + + if (!self.aggType || !self.visType || !self.visAggSchema) { + throw new Error('unable to find a usable visType and schema for the ' + opts.aggType + ' agg type'); + } + + self.vis = new Vis(self.indexPattern, { + type: self.visType + }); + } + + AggParamWriter.prototype.write = function (paramValues) { + let self = this; + paramValues = _.clone(paramValues); + + if (self.aggType.params.byName.field && !paramValues.field) { + // pick a field rather than force a field to be specified everywhere + if (self.aggType.type === 'metrics') { + paramValues.field = _.sample(self.indexPattern.fields.byType.number); + } else { + paramValues.field = _.sample(self.indexPattern.fields.byType.string); + } + } + + self.vis.setState({ + type: self.vis.type.name, + aggs: [{ + type: self.aggType, + schema: self.visAggSchema, + params: paramValues + }] + }); + + let aggConfig = _.find(self.vis.aggs, function (aggConfig) { + return aggConfig.type === self.aggType; + }); + + aggConfig.type.params.forEach(function (param) { + if (param.onRequest) { + param.onRequest(aggConfig); + } + }); + + return aggConfig.type.params.write(aggConfig); + }; + + return AggParamWriter; + +}; diff --git a/src/ui/public/agg_types/__tests__/agg_params.js b/src/ui/public/agg_types/__tests__/agg_params.js new file mode 100644 index 0000000000000..1eff825ecb769 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/agg_params.js @@ -0,0 +1,107 @@ +import _ from 'lodash'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import AggTypesAggParamsProvider from 'ui/agg_types/agg_params'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesFieldProvider from 'ui/agg_types/param_types/field'; +import AggTypesParamTypesOptionedProvider from 'ui/agg_types/param_types/optioned'; +import AggTypesParamTypesRegexProvider from 'ui/agg_types/param_types/regex'; +describe('AggParams class', function () { + + let AggParams; + let BaseAggParam; + let FieldAggParam; + let OptionedAggParam; + let RegexAggParam; + + beforeEach(ngMock.module('kibana')); + // stub out the param classes before we get the AggParams + beforeEach(ngMock.inject(require('./utils/_stub_agg_params'))); + // fetch out deps + beforeEach(ngMock.inject(function (Private) { + AggParams = Private(AggTypesAggParamsProvider); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + FieldAggParam = Private(AggTypesParamTypesFieldProvider); + OptionedAggParam = Private(AggTypesParamTypesOptionedProvider); + RegexAggParam = Private(AggTypesParamTypesRegexProvider); + })); + + describe('constructor args', function () { + it('accepts an array of param defs', function () { + let params = [ + { name: 'one' }, + { name: 'two' } + ]; + let aggParams = new AggParams(params); + + expect(aggParams).to.have.length(params.length); + expect(aggParams).to.be.an(Array); + expect(aggParams.byName).to.have.keys(['one', 'two']); + }); + }); + + describe('AggParam creation', function () { + it('Uses the FieldAggParam class for params with the name "field"', function () { + let params = [ + { name: 'field' } + ]; + let aggParams = new AggParams(params); + + expect(aggParams).to.have.length(params.length); + expect(aggParams[0]).to.be.a(FieldAggParam); + }); + + it('Uses the OptionedAggParam class for params of type "optioned"', function () { + let params = [ + { + name: 'interval', + type: 'optioned' + } + ]; + let aggParams = new AggParams(params); + + expect(aggParams).to.have.length(params.length); + expect(aggParams[0]).to.be.a(OptionedAggParam); + }); + + it('Uses the RegexAggParam class for params of type "regex"', function () { + let params = [ + { + name: 'exclude', + type: 'regex' + } + ]; + let aggParams = new AggParams(params); + + expect(aggParams).to.have.length(params.length); + expect(aggParams[0]).to.be.a(RegexAggParam); + }); + + it('Always converts the params to a BaseAggParam', function () { + let params = [ + { + name: 'height', + editor: 'high' + }, + { + name: 'weight', + editor: 'big' + }, + { + name: 'waist', + editor: 'small' + } + ]; + let aggParams = new AggParams(params); + + expect(BaseAggParam).to.have.property('callCount', params.length); + expect(FieldAggParam).to.have.property('callCount', 0); + expect(OptionedAggParam).to.have.property('callCount', 0); + + expect(aggParams).to.have.length(params.length); + aggParams.forEach(function (aggParam) { + expect(aggParam).to.be.a(BaseAggParam); + }); + }); + }); +}); diff --git a/src/ui/public/agg_types/__tests__/agg_type.js b/src/ui/public/agg_types/__tests__/agg_type.js new file mode 100644 index 0000000000000..94dbcab47d495 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/agg_type.js @@ -0,0 +1,190 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'auto-release-sinon'; +import 'ui/private'; +import AggParamsPM from 'ui/agg_types/agg_params'; +import VisProvider from 'ui/vis'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import AggTypesAggTypeProvider from 'ui/agg_types/agg_type'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +describe('AggType Class', function () { + let AggType; + let AggParams; + let AggConfig; + let indexPattern; + let fieldFormat; + let Vis; + + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + AggParams = sinon.spy(Private(AggParamsPM)); + Private.stub(AggParamsPM, AggParams); + + Vis = Private(VisProvider); + fieldFormat = Private(RegistryFieldFormatsProvider); + AggType = Private(AggTypesAggTypeProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + })); + + describe('constructor', function () { + + it('requires a config object as it\'s first param', function () { + expect(function () { + new AggType(null); + }).to.throwError(); + }); + + describe('application of config properties', function () { + let copiedConfigProps = [ + 'name', + 'title', + 'makeLabel', + 'ordered' + ]; + + describe('"' + copiedConfigProps.join('", "') + '"', function () { + it('assigns the config value to itself', function () { + let config = _.transform(copiedConfigProps, function (config, prop) { + config[prop] = {}; + }, {}); + + let aggType = new AggType(config); + + copiedConfigProps.forEach(function (prop) { + expect(aggType[prop]).to.be(config[prop]); + }); + }); + }); + + describe('makeLabel', function () { + it('makes a function when the makeLabel config is not specified', function () { + let someGetter = function () {}; + + let aggType = new AggType({ + makeLabel: someGetter + }); + + expect(aggType.makeLabel).to.be(someGetter); + + aggType = new AggType({ + name: 'pizza' + }); + + expect(aggType.makeLabel).to.be.a('function'); + expect(aggType.makeLabel()).to.be('pizza'); + }); + }); + + describe('getFormat', function () { + it('returns the formatter for the aggConfig', function () { + let aggType = new AggType({}); + + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + } + ] + }); + + let aggConfig = vis.aggs.byTypeName.date_histogram[0]; + + expect(aggType.getFormat(aggConfig)).to.be(fieldFormat.getDefaultInstance('date')); + + vis = new Vis(indexPattern, { + type: 'metric', + aggs: [ + { + type: 'count', + schema: 'metric' + } + ] + }); + aggConfig = vis.aggs.byTypeName.count[0]; + + expect(aggType.getFormat(aggConfig)).to.be(fieldFormat.getDefaultInstance('string')); + }); + + it('can be overridden via config', function () { + let someGetter = function () {}; + + let aggType = new AggType({ + getFormat: someGetter + }); + + expect(aggType.getFormat).to.be(someGetter); + }); + }); + + describe('params', function () { + beforeEach(function () { + AggParams.reset(); + }); + + it('defaults to AggParams object with JSON param', function () { + let aggType = new AggType({ + name: 'smart agg' + }); + + expect(aggType.params).to.be.an(AggParams); + expect(aggType.params.length).to.be(2); + expect(aggType.params[0].name).to.be('json'); + expect(aggType.params[1].name).to.be('customLabel'); + }); + + it('can disable customLabel', function () { + let aggType = new AggType({ + name: 'smart agg', + customLabels: false + }); + + expect(aggType.params.length).to.be(1); + expect(aggType.params[0].name).to.be('json'); + }); + + it('passes the params arg directly to the AggParams constructor', function () { + let params = [ + {name: 'one'}, + {name: 'two'} + ]; + let paramLength = params.length + 2; // json and custom label are always appended + + let aggType = new AggType({ + name: 'bucketeer', + params: params + }); + + expect(aggType.params).to.be.an(AggParams); + expect(aggType.params.length).to.be(paramLength); + expect(AggParams.callCount).to.be(1); + expect(AggParams.firstCall.args[0]).to.be(params); + }); + }); + + describe('getResponseAggs', function () { + it('copies the value', function () { + let football = {}; + let aggType = new AggType({ + getResponseAggs: football + }); + + expect(aggType.getResponseAggs).to.be(football); + }); + + it('defaults to _.noop', function () { + let aggType = new AggType({}); + + expect(aggType.getResponseAggs).to.be(_.noop); + }); + }); + }); + + }); + +}); diff --git a/src/ui/public/agg_types/__tests__/bucketCountBetween.js b/src/ui/public/agg_types/__tests__/bucketCountBetween.js deleted file mode 100644 index 45f716b99e408..0000000000000 --- a/src/ui/public/agg_types/__tests__/bucketCountBetween.js +++ /dev/null @@ -1,205 +0,0 @@ -describe('bucketCountBetween util', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var indexPattern; - var Vis; - var visTypes; - var aggTypes; - var AggConfig; - var bucketCountBetween; - - // http://cwestblog.com/2014/02/25/javascript-testing-for-negative-zero/ - // works for -0 and +0 - function isNegative(n) { - return ((n = +n) || 1 / n) < 0; - } - - beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - Vis = Private(require('ui/Vis')); - visTypes = Private(require('ui/registry/vis_types')); - aggTypes = Private(require('ui/agg_types/index')); - AggConfig = Private(require('ui/Vis/AggConfig')); - bucketCountBetween = Private(require('ui/agg_types/buckets/_bucket_count_between')); - })); - - it('returns a positive number when a is before b', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - }, - { - type: 'terms', - schema: 'segment' - } - ] - }); - - var a = vis.aggs.byTypeName.date_histogram[0]; - var b = vis.aggs.byTypeName.terms[0]; - var count = bucketCountBetween(a, b); - expect(isNegative(count)).to.be(false); - }); - - it('returns a negative number when a is after b', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - }, - { - type: 'terms', - schema: 'segment' - } - ] - }); - - var a = vis.aggs.byTypeName.terms[0]; - var b = vis.aggs.byTypeName.date_histogram[0]; - var count = bucketCountBetween(a, b); - expect(isNegative(count)).to.be(true); - }); - - it('returns 0 when there are no buckets between a and b', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - }, - { - type: 'terms', - schema: 'segment' - } - ] - }); - - var a = vis.aggs.byTypeName.date_histogram[0]; - var b = vis.aggs.byTypeName.terms[0]; - expect(bucketCountBetween(a, b)).to.be(0); - }); - - it('returns null when b is not in the aggs', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var a = vis.aggs.byTypeName.date_histogram[0]; - var b = new AggConfig(vis, { - type: 'terms', - schema: 'segment' - }); - - expect(bucketCountBetween(a, b)).to.be(null); - }); - - it('returns null when a is not in the aggs', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [ - { - type: 'date_histogram', - schema: 'segment' - } - ] - }); - - var a = new AggConfig(vis, { - type: 'terms', - schema: 'segment' - }); - var b = vis.aggs.byTypeName.date_histogram[0]; - - expect(bucketCountBetween(a, b)).to.be(null); - }); - - it('returns null when a and b are not in the aggs', function () { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [] - }); - - var a = new AggConfig(vis, { - type: 'terms', - schema: 'segment' - }); - - var b = new AggConfig(vis, { - type: 'date_histogram', - schema: 'segment' - }); - - expect(bucketCountBetween(a, b)).to.be(null); - }); - - function countTest(pre, post) { - return function () { - var schemas = visTypes.byName.histogram.schemas.buckets; - - // slow for this test is actually somewhere around 1/2 a sec - this.slow(500); - - function randBucketAggForVis(vis) { - var schema = _.sample(schemas); - var aggType = _.sample(aggTypes.byType.buckets); - - return new AggConfig(vis, { - schema: schema, - type: aggType - }); - } - - _.times(50, function (n) { - var vis = new Vis(indexPattern, { - type: 'histogram', - aggs: [] - }); - - var randBucketAgg = _.partial(randBucketAggForVis, vis); - - var a = randBucketAgg(); - var b = randBucketAgg(); - - // create n aggs between a and b - var aggs = []; - aggs.fill = function (n) { - for (var i = 0; i < n; i++) { - aggs.push(randBucketAgg()); - } - }; - - pre && aggs.fill(_.random(0, 10)); - aggs.push(a); - aggs.fill(n); - aggs.push(b); - post && aggs.fill(_.random(0, 10)); - - vis.setState({ - type: 'histogram', - aggs: aggs - }); - - expect(bucketCountBetween(a, b)).to.be(n); - }); - }; - } - - it('can count', countTest()); - it('can count with elements before', countTest(true)); - it('can count with elements after', countTest(false, true)); - it('can count with elements before and after', countTest(true, true)); -}); diff --git a/src/ui/public/agg_types/__tests__/bucket_count_between.js b/src/ui/public/agg_types/__tests__/bucket_count_between.js new file mode 100644 index 0000000000000..75c2a4e46fdf7 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/bucket_count_between.js @@ -0,0 +1,211 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import VisProvider from 'ui/vis'; +import RegistryVisTypesProvider from 'ui/registry/vis_types'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import AggTypesBucketsBucketCountBetweenProvider from 'ui/agg_types/buckets/_bucket_count_between'; +describe('bucketCountBetween util', function () { + let indexPattern; + let Vis; + let visTypes; + let aggTypes; + let AggConfig; + let bucketCountBetween; + + // http://cwestblog.com/2014/02/25/javascript-testing-for-negative-zero/ + // works for -0 and +0 + function isNegative(n) { + return ((n = +n) || 1 / n) < 0; + } + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + Vis = Private(VisProvider); + visTypes = Private(RegistryVisTypesProvider); + aggTypes = Private(AggTypesIndexProvider); + AggConfig = Private(VisAggConfigProvider); + bucketCountBetween = Private(AggTypesBucketsBucketCountBetweenProvider); + })); + + it('returns a positive number when a is before b', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + }, + { + type: 'terms', + schema: 'segment' + } + ] + }); + + let a = vis.aggs.byTypeName.date_histogram[0]; + let b = vis.aggs.byTypeName.terms[0]; + let count = bucketCountBetween(a, b); + expect(isNegative(count)).to.be(false); + }); + + it('returns a negative number when a is after b', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + }, + { + type: 'terms', + schema: 'segment' + } + ] + }); + + let a = vis.aggs.byTypeName.terms[0]; + let b = vis.aggs.byTypeName.date_histogram[0]; + let count = bucketCountBetween(a, b); + expect(isNegative(count)).to.be(true); + }); + + it('returns 0 when there are no buckets between a and b', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + }, + { + type: 'terms', + schema: 'segment' + } + ] + }); + + let a = vis.aggs.byTypeName.date_histogram[0]; + let b = vis.aggs.byTypeName.terms[0]; + expect(bucketCountBetween(a, b)).to.be(0); + }); + + it('returns null when b is not in the aggs', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + } + ] + }); + + let a = vis.aggs.byTypeName.date_histogram[0]; + let b = new AggConfig(vis, { + type: 'terms', + schema: 'segment' + }); + + expect(bucketCountBetween(a, b)).to.be(null); + }); + + it('returns null when a is not in the aggs', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { + type: 'date_histogram', + schema: 'segment' + } + ] + }); + + let a = new AggConfig(vis, { + type: 'terms', + schema: 'segment' + }); + let b = vis.aggs.byTypeName.date_histogram[0]; + + expect(bucketCountBetween(a, b)).to.be(null); + }); + + it('returns null when a and b are not in the aggs', function () { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [] + }); + + let a = new AggConfig(vis, { + type: 'terms', + schema: 'segment' + }); + + let b = new AggConfig(vis, { + type: 'date_histogram', + schema: 'segment' + }); + + expect(bucketCountBetween(a, b)).to.be(null); + }); + + function countTest(pre, post) { + return function () { + let schemas = visTypes.byName.histogram.schemas.buckets; + + // slow for this test is actually somewhere around 1/2 a sec + this.slow(500); + + function randBucketAggForVis(vis) { + let schema = _.sample(schemas); + let aggType = _.sample(aggTypes.byType.buckets); + + return new AggConfig(vis, { + schema: schema, + type: aggType + }); + } + + _.times(50, function (n) { + let vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [] + }); + + let randBucketAgg = _.partial(randBucketAggForVis, vis); + + let a = randBucketAgg(); + let b = randBucketAgg(); + + // create n aggs between a and b + let aggs = []; + aggs.fill = function (n) { + for (let i = 0; i < n; i++) { + aggs.push(randBucketAgg()); + } + }; + + pre && aggs.fill(_.random(0, 10)); + aggs.push(a); + aggs.fill(n); + aggs.push(b); + post && aggs.fill(_.random(0, 10)); + + vis.setState({ + type: 'histogram', + aggs: aggs + }); + + expect(bucketCountBetween(a, b)).to.be(n); + }); + }; + } + + it('can count', countTest()); + it('can count with elements before', countTest(true)); + it('can count with elements after', countTest(false, true)); + it('can count with elements before and after', countTest(true, true)); +}); diff --git a/src/ui/public/agg_types/__tests__/buckets/_histogram.js b/src/ui/public/agg_types/__tests__/buckets/_histogram.js index 5da2bf18b56a1..325e91ecf56c9 100644 --- a/src/ui/public/agg_types/__tests__/buckets/_histogram.js +++ b/src/ui/public/agg_types/__tests__/buckets/_histogram.js @@ -1,14 +1,16 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import AggParamWriterProvider from '../agg_param_writer'; describe('Histogram Agg', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); describe('ordered', function () { - var histogram; + let histogram; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - histogram = Private(require('ui/agg_types/index')).byName.histogram; + histogram = Private(AggTypesIndexProvider).byName.histogram; })); it('is ordered', function () { @@ -22,11 +24,11 @@ describe('Histogram Agg', function () { describe('params', function () { - var paramWriter; + let paramWriter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - var AggParamWriter = Private(require('../AggParamWriter')); + let AggParamWriter = Private(AggParamWriterProvider); paramWriter = new AggParamWriter({ aggType: 'histogram' }); })); @@ -34,25 +36,25 @@ describe('Histogram Agg', function () { // reads aggConfig.params.interval, writes to dsl.interval it('accepts a number', function () { - var output = paramWriter.write({ interval: 100 }); + let output = paramWriter.write({ interval: 100 }); expect(output.params).to.have.property('interval', 100); }); it('accepts a string', function () { - var output = paramWriter.write({ interval: '10' }); + let output = paramWriter.write({ interval: '10' }); expect(output.params).to.have.property('interval', 10); }); it('fails on non-numeric values', function () { // template validation prevents this from users, not devs - var output = paramWriter.write({ interval: [] }); + let output = paramWriter.write({ interval: [] }); expect(isNaN(output.params.interval)).to.be.ok(); }); }); describe('min_doc_count', function () { it('casts true values to 0', function () { - var output = paramWriter.write({ min_doc_count: true }); + let output = paramWriter.write({ min_doc_count: true }); expect(output.params).to.have.property('min_doc_count', 0); output = paramWriter.write({ min_doc_count: 'yes' }); @@ -66,7 +68,7 @@ describe('Histogram Agg', function () { }); it('writes nothing for false values', function () { - var output = paramWriter.write({ min_doc_count: '' }); + let output = paramWriter.write({ min_doc_count: '' }); expect(output.params).to.not.have.property('min_doc_count'); output = paramWriter.write({ min_doc_count: null }); @@ -79,7 +81,8 @@ describe('Histogram Agg', function () { describe('extended_bounds', function () { it('writes when only eb.min is set', function () { - var output = paramWriter.write({ + let output = paramWriter.write({ + min_doc_count: true, extended_bounds: { min: 0 } }); expect(output.params.extended_bounds).to.have.property('min', 0); @@ -87,7 +90,8 @@ describe('Histogram Agg', function () { }); it('writes when only eb.max is set', function () { - var output = paramWriter.write({ + let output = paramWriter.write({ + min_doc_count: true, extended_bounds: { max: 0 } }); expect(output.params.extended_bounds).to.have.property('min', undefined); @@ -95,7 +99,8 @@ describe('Histogram Agg', function () { }); it('writes when both eb.min and eb.max are set', function () { - var output = paramWriter.write({ + let output = paramWriter.write({ + min_doc_count: true, extended_bounds: { min: 99, max: 100 } }); expect(output.params.extended_bounds).to.have.property('min', 99); @@ -103,11 +108,20 @@ describe('Histogram Agg', function () { }); it('does not write when nothing is set', function () { - var output = paramWriter.write({ + let output = paramWriter.write({ + min_doc_count: true, extended_bounds: {} }); expect(output.params).to.not.have.property('extended_bounds'); }); + + it('does not write when min_doc_count is false', function () { + let output = paramWriter.write({ + min_doc_count: false, + extended_bounds: { min: 99, max: 100 } + }); + expect(output.params).to.not.have.property('extended_bounds'); + }); }); }); }); diff --git a/src/ui/public/agg_types/__tests__/buckets/_range.js b/src/ui/public/agg_types/__tests__/buckets/_range.js index 998b0857ba642..a25c447bdd38f 100644 --- a/src/ui/public/agg_types/__tests__/buckets/_range.js +++ b/src/ui/public/agg_types/__tests__/buckets/_range.js @@ -1,23 +1,27 @@ +import { values } from 'lodash'; +import _ from 'lodash'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import resp from 'fixtures/agg_resp/range'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; describe('Range Agg', function () { - var _ = require('lodash'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - var values = require('lodash').values; - var resp = require('fixtures/agg_resp/range'); - var buckets = values(resp.aggregations[1].buckets); + let buckets = values(resp.aggregations[1].buckets); - var range; - var Vis; - var indexPattern; + let range; + let Vis; + let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - range = Private(require('ui/agg_types/index')).byName.range; - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + range = Private(AggTypesIndexProvider).byName.range; + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - var BytesFormat = Private(require('ui/registry/field_formats')).byId.bytes; + let BytesFormat = Private(RegistryFieldFormatsProvider).byId.bytes; indexPattern.fieldFormatMap.bytes = new BytesFormat({ pattern: '0,0.[000] b' @@ -28,7 +32,7 @@ describe('Range Agg', function () { describe('formating', function () { it('formats bucket keys properly', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -45,8 +49,8 @@ describe('Range Agg', function () { ] }); - var agg = vis.aggs.byTypeName.range[0]; - var format = function (val) { + let agg = vis.aggs.byTypeName.range[0]; + let format = function (val) { return agg.fieldFormatter()(agg.getKey(val)); }; expect(format(buckets[0])).to.be('-∞ to 1 KB'); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/date_histogram.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/date_histogram.js index 21da090d58cd8..9efc4515038aa 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/date_histogram.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/date_histogram.js @@ -1,30 +1,35 @@ +import _ from 'lodash'; +import moment from 'moment'; +import sinon from 'auto-release-sinon'; +import aggResp from 'fixtures/agg_resp/date_histogram'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterDateHistogramProvider from 'ui/agg_types/buckets/create_filter/date_histogram'; +import TimeBucketsProvider from 'ui/time_buckets'; +import AggTypesBucketsIntervalOptionsProvider from 'ui/agg_types/buckets/_interval_options'; describe('AggConfig Filters', function () { describe('date_histogram', function () { - var _ = require('lodash'); - var moment = require('moment'); - var sinon = require('auto-release-sinon'); - var aggResp = require('fixtures/agg_resp/date_histogram'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - - var vis; - var agg; - var field; - var filter; - var bucketKey; - var bucketStart; - var getIntervalStub; - var intervalOptions; - - var init; + + let vis; + let agg; + let field; + let filter; + let bucketKey; + let bucketStart; + let getIntervalStub; + let intervalOptions; + + let init; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - var Vis = Private(require('ui/Vis')); - var indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/date_histogram')); - var TimeBuckets = Private(require('ui/time_buckets')); - intervalOptions = Private(require('ui/agg_types/buckets/_interval_options')); + let Vis = Private(VisProvider); + let indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + let createFilter = Private(AggTypesBucketsCreateFilterDateHistogramProvider); + let TimeBuckets = Private(TimeBucketsProvider); + intervalOptions = Private(AggTypesBucketsIntervalOptionsProvider); init = function (interval, duration) { interval = interval || 'auto'; @@ -46,7 +51,7 @@ describe('AggConfig Filters', function () { bucketKey = _.sample(aggResp.aggregations['1'].buckets).key; bucketStart = moment(bucketKey); - var timePad = moment.duration(duration / 2); + let timePad = moment.duration(duration / 2); agg.buckets.setBounds({ min: bucketStart.clone().subtract(timePad), max: bucketStart.clone().add(timePad), @@ -63,14 +68,17 @@ describe('AggConfig Filters', function () { expect(filter).to.have.property('range'); expect(filter.range).to.have.property(field.name); - var fieldParams = filter.range[field.name]; + let fieldParams = filter.range[field.name]; expect(fieldParams).to.have.property('gte'); expect(fieldParams.gte).to.be.a('number'); - expect(fieldParams).to.have.property('lte'); - expect(fieldParams.lte).to.be.a('number'); + expect(fieldParams).to.have.property('lt'); + expect(fieldParams.lt).to.be.a('number'); + + expect(fieldParams).to.have.property('format'); + expect(fieldParams.format).to.be('epoch_millis'); - expect(fieldParams.gte).to.be.lessThan(fieldParams.lte); + expect(fieldParams.gte).to.be.lessThan(fieldParams.lt); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', vis.indexPattern.id); @@ -79,7 +87,7 @@ describe('AggConfig Filters', function () { it('extends the filter edge to 1ms before the next bucket for all interval options', function () { intervalOptions.forEach(function (option) { - var duration; + let duration; if (option.val !== 'custom' && moment(1, option.val).isValid()) { duration = moment.duration(10, option.val); @@ -90,11 +98,11 @@ describe('AggConfig Filters', function () { init(option.val, duration); - var interval = agg.buckets.getInterval(); - var params = filter.range[field.name]; + let interval = agg.buckets.getInterval(); + let params = filter.range[field.name]; expect(params.gte).to.be(+bucketStart); - expect(params.lte).to.be(+bucketStart.clone().add(interval).subtract(1, 'ms')); + expect(params.lt).to.be(+bucketStart.clone().add(interval)); }); }); }); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/date_range.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/date_range.js index d86bb6ad9e552..117852228b6cc 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/date_range.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/date_range.js @@ -1,25 +1,29 @@ -var moment = require('moment'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import moment from 'moment'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterDateRangeProvider from 'ui/agg_types/buckets/create_filter/date_range'; describe('AggConfig Filters', function () { describe('Date range', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/date_range')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterDateRangeProvider); })); it('should return a range filter for date_range agg', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -34,8 +38,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.date_range[0]; - var filter = createFilter(aggConfig, 'February 1st, 2015 to February 7th, 2015'); + let aggConfig = vis.aggs.byTypeName.date_range[0]; + let filter = createFilter(aggConfig, 'February 1st, 2015 to February 7th, 2015'); expect(filter).to.have.property('range'); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', indexPattern.id); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/filters.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/filters.js index c891a95c7d157..43069575965af 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/filters.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/filters.js @@ -1,25 +1,29 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterFiltersProvider from 'ui/agg_types/buckets/create_filter/filters'; describe('AggConfig Filters', function () { describe('filters', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/filters')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterFiltersProvider); })); it('should return a filters filter', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -35,8 +39,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.filters[0]; - var filter = createFilter(aggConfig, '_type:nginx'); + let aggConfig = vis.aggs.byTypeName.filters[0]; + let filter = createFilter(aggConfig, '_type:nginx'); expect(_.omit(filter, 'meta')).to.eql(aggConfig.params.filters[1].input); expect(filter.meta).to.have.property('index', indexPattern.id); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/histogram.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/histogram.js index 076386dd4ed69..702cd0ea746b7 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/histogram.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/histogram.js @@ -1,25 +1,29 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterHistogramProvider from 'ui/agg_types/buckets/create_filter/histogram'; describe('AggConfig Filters', function () { describe('histogram', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/histogram')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterHistogramProvider); })); it('should return an range filter for histogram', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -30,8 +34,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.histogram[0]; - var filter = createFilter(aggConfig, 2048); + let aggConfig = vis.aggs.byTypeName.histogram[0]; + let filter = createFilter(aggConfig, 2048); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', indexPattern.id); expect(filter).to.have.property('range'); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/ip_range.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/ip_range.js index c58bcff71dbb8..d98e8f0727efc 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/ip_range.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/ip_range.js @@ -1,23 +1,27 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterIpRangeProvider from 'ui/agg_types/buckets/create_filter/ip_range'; describe('AggConfig Filters', function () { - var expect = require('expect.js'); - var ngMock = require('ngMock'); describe('IP range', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/ip_range')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterIpRangeProvider); })); it('should return a range filter for ip_range agg', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -36,8 +40,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.ip_range[0]; - var filter = createFilter(aggConfig, '0.0.0.0-1.1.1.1'); + let aggConfig = vis.aggs.byTypeName.ip_range[0]; + let filter = createFilter(aggConfig, '0.0.0.0-1.1.1.1'); expect(filter).to.have.property('range'); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', indexPattern.id); @@ -47,7 +51,7 @@ describe('AggConfig Filters', function () { }); it('should return a range filter for ip_range agg using a CIDR mask', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -66,8 +70,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.ip_range[0]; - var filter = createFilter(aggConfig, '67.129.65.201/27'); + let aggConfig = vis.aggs.byTypeName.ip_range[0]; + let filter = createFilter(aggConfig, '67.129.65.201/27'); expect(filter).to.have.property('range'); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', indexPattern.id); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/range.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/range.js index ca5b8eff5f745..b6ccd71d38cd5 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/range.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/range.js @@ -1,23 +1,27 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterRangeProvider from 'ui/agg_types/buckets/create_filter/range'; describe('AggConfig Filters', function () { - var expect = require('expect.js'); - var ngMock = require('ngMock'); describe('range', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/range')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterRangeProvider); })); it('should return a range filter for range agg', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { @@ -33,8 +37,8 @@ describe('AggConfig Filters', function () { ] }); - var aggConfig = vis.aggs.byTypeName.range[0]; - var filter = createFilter(aggConfig, { gte: 1024, lt: 2048.0 }); + let aggConfig = vis.aggs.byTypeName.range[0]; + let filter = createFilter(aggConfig, { gte: 1024, lt: 2048.0 }); expect(filter).to.have.property('range'); expect(filter).to.have.property('meta'); expect(filter.meta).to.have.property('index', indexPattern.id); diff --git a/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js b/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js index 32a9fe897921d..c0a67aa283655 100644 --- a/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js +++ b/src/ui/public/agg_types/__tests__/buckets/create_filter/terms.js @@ -1,28 +1,32 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesBucketsCreateFilterTermsProvider from 'ui/agg_types/buckets/create_filter/terms'; describe('AggConfig Filters', function () { - var expect = require('expect.js'); - var ngMock = require('ngMock'); describe('terms', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - createFilter = Private(require('ui/agg_types/buckets/create_filter/terms')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); })); it('should return a match filter for terms', function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'terms', schema: 'segment', params: { field: '_type' } } ] }); - var aggConfig = vis.aggs.byTypeName.terms[0]; - var filter = createFilter(aggConfig, 'apache'); + let aggConfig = vis.aggs.byTypeName.terms[0]; + let filter = createFilter(aggConfig, 'apache'); expect(filter).to.have.property('query'); expect(filter.query).to.have.property('match'); expect(filter.query.match).to.have.property('_type'); diff --git a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_date_histogram.js b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_date_histogram.js index 48c444f5d3b66..e2e65bb1f4e97 100644 --- a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_date_histogram.js +++ b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_date_histogram.js @@ -1,4 +1,4 @@ +import './_editor'; +import './_params'; describe('Date Histogram Agg', function () { - require('./_editor'); - require('./_params'); }); diff --git a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js index 88152a39f55e3..6d2e4834418eb 100644 --- a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js +++ b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_editor.js @@ -1,20 +1,23 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import VisProvider from 'ui/vis'; +import AggTypesBucketsIntervalOptionsProvider from 'ui/agg_types/buckets/_interval_options'; describe('editor', function () { - var _ = require('lodash'); - var $ = require('jquery'); - var ngMock = require('ngMock'); - var expect = require('expect.js'); - var indexPattern; - var vis; - var agg; - var render; - var $scope; + let indexPattern; + let vis; + let agg; + let render; + let $scope; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector, $compile) { - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - var Vis = Private(require('ui/Vis')); + let Vis = Private(VisProvider); /** * Render the AggParams editor for the date histogram aggregation @@ -35,8 +38,8 @@ describe('editor', function () { ] }); - var $el = $(''); - var $parentScope = $injector.get('$rootScope').$new(); + let $el = $(''); + let $parentScope = $injector.get('$rootScope').$new(); agg = $parentScope.agg = vis.aggs.bySchemaName.segment[0]; $parentScope.groupName = 'buckets'; @@ -45,10 +48,10 @@ describe('editor', function () { $scope = $el.scope(); $scope.$digest(); - var $inputs = $('vis-agg-param-editor', $el); + let $inputs = $('vis-agg-param-editor', $el); return _.transform($inputs.toArray(), function (inputs, e) { - var $el = $(e); - var $scope = $el.scope(); + let $el = $(e); + let $scope = $el.scope(); inputs[$scope.aggParam.name] = { $el: $el, @@ -66,13 +69,13 @@ describe('editor', function () { })); describe('random field/interval', function () { - var params; - var field; - var interval; + let params; + let field; + let interval; beforeEach(ngMock.inject(function (Private) { field = _.sample(indexPattern.fields); - interval = _.sample(Private(require('ui/agg_types/buckets/_interval_options'))); + interval = _.sample(Private(AggTypesBucketsIntervalOptionsProvider)); params = render({ field: field, interval: interval }); })); @@ -94,7 +97,7 @@ describe('editor', function () { }); describe('interval "auto" and indexPattern timeField', function () { - var params; + let params; beforeEach(function () { params = render({ field: indexPattern.timeFieldName, interval: 'auto' }); @@ -104,7 +107,7 @@ describe('editor', function () { expect(params.interval.modelValue().val).to.be('auto'); expect(params.field.modelValue().name).to.be(indexPattern.timeFieldName); - var field = _.find(indexPattern.fields, function (f) { + let field = _.find(indexPattern.fields, function (f) { return f.type === 'date' && f.name !== indexPattern.timeFieldName; }); diff --git a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js index 4ad58ff3d3026..4e6c6870f2574 100644 --- a/src/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js +++ b/src/ui/public/agg_types/__tests__/buckets/date_histogram/_params.js @@ -1,33 +1,37 @@ +import _ from 'lodash'; +import moment from 'moment'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggParamWriterProvider from '../../agg_param_writer'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import VisAggConfigProvider from 'ui/vis/agg_config'; describe('params', function () { - var _ = require('lodash'); - var moment = require('moment'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var paramWriter; - var writeInterval; + let paramWriter; + let writeInterval; - var aggTypes; - var AggConfig; - var setTimeBounds; - var timeField; + let aggTypes; + let AggConfig; + let setTimeBounds; + let timeField; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { - var AggParamWriter = Private(require('../../AggParamWriter')); - var indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - var timefilter = $injector.get('timefilter'); + let AggParamWriter = Private(AggParamWriterProvider); + let indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + let timefilter = $injector.get('timefilter'); timeField = indexPattern.timeFieldName; - aggTypes = Private(require('ui/agg_types/index')); - AggConfig = Private(require('ui/Vis/AggConfig')); + aggTypes = Private(AggTypesIndexProvider); + AggConfig = Private(VisAggConfigProvider); paramWriter = new AggParamWriter({ aggType: 'date_histogram' }); writeInterval = function (interval) { return paramWriter.write({ interval: interval, field: timeField }); }; - var now = moment(); + let now = moment(); setTimeBounds = function (n, units) { timefilter.enabled = true; timefilter.getBounds = _.constant({ @@ -39,24 +43,24 @@ describe('params', function () { describe('interval', function () { it('accepts a valid interval', function () { - var output = writeInterval('d'); + let output = writeInterval('d'); expect(output.params).to.have.property('interval', '1d'); }); it('ignores invalid intervals', function () { - var output = writeInterval('foo'); + let output = writeInterval('foo'); expect(output.params).to.have.property('interval', '0ms'); }); it('automatically picks an interval', function () { setTimeBounds(15, 'm'); - var output = writeInterval('auto'); + let output = writeInterval('auto'); expect(output.params.interval).to.be('30s'); }); it('scales up the interval if it will make too many buckets', function () { setTimeBounds(30, 'm'); - var output = writeInterval('s'); + let output = writeInterval('s'); expect(output.params.interval).to.be('10s'); expect(output.metricScaleText).to.be('second'); expect(output.metricScale).to.be(0.1); @@ -64,30 +68,30 @@ describe('params', function () { it('does not scale down the interval', function () { setTimeBounds(1, 'm'); - var output = writeInterval('h'); + let output = writeInterval('h'); expect(output.params.interval).to.be('1h'); expect(output.metricScaleText).to.be(undefined); expect(output.metricScale).to.be(undefined); }); describe('only scales when all metrics are sum or count', function () { - var tests = [ + let tests = [ [ false, 'avg', 'count', 'sum' ], [ true, 'count', 'sum' ], [ false, 'count', 'cardinality' ] ]; tests.forEach(function (test) { - var should = test.shift(); - var typeNames = test.slice(); + let should = test.shift(); + let typeNames = test.slice(); it(typeNames.join(', ') + ' should ' + (should ? '' : 'not') + ' scale', function () { setTimeBounds(1, 'y'); - var vis = paramWriter.vis; + let vis = paramWriter.vis; vis.aggs.splice(0); - var histoConfig = new AggConfig(vis, { + let histoConfig = new AggConfig(vis, { type: aggTypes.byName.date_histogram, schema: 'segment', params: { interval: 's', field: timeField } @@ -102,7 +106,7 @@ describe('params', function () { })); }); - var output = histoConfig.write(); + let output = histoConfig.write(); expect(_.has(output, 'metricScale')).to.be(should); }); }); @@ -111,9 +115,9 @@ describe('params', function () { describe('extended_bounds', function () { it('should write a long value if a moment passed in', function () { - var then = moment(0); - var now = moment(500); - var output = paramWriter.write({ + let then = moment(0); + let now = moment(500); + let output = paramWriter.write({ extended_bounds: { min: then, max: now @@ -129,9 +133,9 @@ describe('params', function () { }); it('should write a long if a long is passed', function () { - var then = 0; - var now = 500; - var output = paramWriter.write({ + let then = 0; + let now = 500; + let output = paramWriter.write({ extended_bounds: { min: then, max: now diff --git a/src/ui/public/agg_types/__tests__/controls/number_list.js b/src/ui/public/agg_types/__tests__/controls/number_list.js index 8ea2d47d297f2..3c7204b0682b8 100644 --- a/src/ui/public/agg_types/__tests__/controls/number_list.js +++ b/src/ui/public/agg_types/__tests__/controls/number_list.js @@ -1,27 +1,27 @@ +import $ from 'jquery'; +import _ from 'lodash'; +import expect from 'expect.js'; +import simulateKeys from 'test_utils/simulate_keys'; +import ngMock from 'ng_mock'; +import 'ui/number_list'; describe('NumberList directive', function () { - var $ = require('jquery'); - var _ = require('lodash'); - var expect = require('expect.js'); - var simulateKeys = require('testUtils/simulateKeys'); - var ngMock = require('ngMock'); - require('ui/number_list'); - var $el; - var $scope; - var compile; + let $el; + let $scope; + let compile; function onlyValidValues() { return $el.find('[ng-model]').toArray().map(function (el) { - var ngModel = $(el).controller('ngModel'); + let ngModel = $(el).controller('ngModel'); return ngModel.$valid ? ngModel.$modelValue : undefined; }); } beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector) { - var $compile = $injector.get('$compile'); - var $rootScope = $injector.get('$rootScope'); + let $compile = $injector.get('$compile'); + let $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); $el = $(''); @@ -75,7 +75,7 @@ describe('NumberList directive', function () { it('shift-up increases by 0.1', function () { compile([4.8]); - var seq = [ + let seq = [ { type: 'press', key: 'shift', @@ -111,7 +111,7 @@ describe('NumberList directive', function () { it('shift-down decreases by 0.1', function () { compile([5.1]); - var seq = [ + let seq = [ { type: 'press', key: 'shift', @@ -135,12 +135,12 @@ describe('NumberList directive', function () { it('maintains valid number', function () { compile([9, 11, 13]); - var seq = [ + let seq = [ 'down', // 10 (11 - 1) 'down' // 10 (limited by 9) ]; - var getEl = function () { return $el.find('input').eq(1); }; + let getEl = function () { return $el.find('input').eq(1); }; return simulateKeys(getEl, seq) .then(function () { diff --git a/src/ui/public/agg_types/__tests__/index.js b/src/ui/public/agg_types/__tests__/index.js index ac1e8c19bb91d..b68dcfc87e284 100644 --- a/src/ui/public/agg_types/__tests__/index.js +++ b/src/ui/public/agg_types/__tests__/index.js @@ -1,21 +1,24 @@ -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import './agg_type'; +import './agg_params'; +import './bucket_count_between'; +import './buckets/_histogram'; +import './buckets/_range'; +import AggTypesIndexProvider from 'ui/agg_types/index'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; describe('AggTypesComponent', function () { - require('./AggType'); - require('./AggParams'); - require('./bucketCountBetween'); - require('./buckets/_histogram'); - require('./buckets/_range'); describe('bucket aggs', function () { - var bucketAggs; - var BucketAggType; + let bucketAggs; + let BucketAggType; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - bucketAggs = Private(require('ui/agg_types/index')).byType.buckets; - BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); + bucketAggs = Private(AggTypesIndexProvider).byType.buckets; + BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); })); it('all extend BucketAggType', function () { @@ -26,13 +29,13 @@ describe('AggTypesComponent', function () { }); describe('metric aggs', function () { - var metricAggs; - var MetricAggType; + let metricAggs; + let MetricAggType; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - metricAggs = Private(require('ui/agg_types/index')).byType.metrics; - MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); + metricAggs = Private(AggTypesIndexProvider).byType.metrics; + MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); })); it('all extend MetricAggType', function () { diff --git a/src/ui/public/agg_types/__tests__/metrics/median.js b/src/ui/public/agg_types/__tests__/metrics/median.js new file mode 100644 index 0000000000000..3524031c62029 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/median.js @@ -0,0 +1,61 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypeMetricMedianProvider from 'ui/agg_types/metrics/median'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggTypeMetricMedianProvider class', function () { + + let vis; + let indexPattern; + let aggTypeMetricMedian; + let aggDsl; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + const Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + aggTypeMetricMedian = Private(AggTypeMetricMedianProvider); + + let vis = new Vis(indexPattern, { + 'title': 'New Visualization', + 'type': 'metric', + 'params': { + 'fontSize': 60, + 'handleNoResults': true + }, + 'aggs': [ + { + 'id': '1', + 'type': 'median', + 'schema': 'metric', + 'params': { + 'field': 'bytes', + 'percents': [ + 50 + ] + } + } + ], + 'listeners': {} + }); + + // Grab the aggConfig off the vis (we don't actually use the vis for + // anything else) + let aggConfig = vis.aggs[0]; + aggDsl = aggConfig.toDsl(); + })); + + it('requests the percentiles aggregation in the Elasticsearch query DSL', function () { + expect(Object.keys(aggDsl)[0]).to.be('percentiles'); + }); + + it ('asks Elasticsearch for the 50th percentile', function () { + expect(aggDsl.percentiles.percents).to.eql([50]); + }); + + it ('asks Elasticsearch for array-based values in the aggregation response', function () { + expect(aggDsl.percentiles.keyed).to.be(false); + }); + +}); diff --git a/src/ui/public/agg_types/__tests__/metrics/percentile_ranks.js b/src/ui/public/agg_types/__tests__/metrics/percentile_ranks.js new file mode 100644 index 0000000000000..048f064dd6ea3 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/percentile_ranks.js @@ -0,0 +1,40 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypeMetricPercentileRanksProvider from 'ui/agg_types/metrics/percentile_ranks'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggTypeMetricPercentileRanksProvider class', function () { + + let Vis; + let indexPattern; + let aggTypeMetricPercentileRanks; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + aggTypeMetricPercentileRanks = Private(AggTypeMetricPercentileRanksProvider); + })); + + it('uses the custom label if it is set', function () { + const vis = new Vis(indexPattern, {}); + + // Grab the aggConfig off the vis (we don't actually use the vis for + // anything else) + const aggConfig = vis.aggs[0]; + aggConfig.params.customLabel = 'my custom field label'; + aggConfig.params.values = [ 5000, 10000 ]; + aggConfig.params.field = { + displayName: 'bytes' + }; + + const responseAggs = aggTypeMetricPercentileRanks.getResponseAggs(aggConfig); + const percentileRankLabelFor5kBytes = responseAggs[0].makeLabel(); + const percentileRankLabelFor10kBytes = responseAggs[1].makeLabel(); + + expect(percentileRankLabelFor5kBytes).to.be('Percentile rank 5,000 of "my custom field label"'); + expect(percentileRankLabelFor10kBytes).to.be('Percentile rank 10,000 of "my custom field label"'); + }); + +}); diff --git a/src/ui/public/agg_types/__tests__/metrics/percentiles.js b/src/ui/public/agg_types/__tests__/metrics/percentiles.js new file mode 100644 index 0000000000000..4b37316ff63e7 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/percentiles.js @@ -0,0 +1,38 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypeMetricPercentilesProvider from 'ui/agg_types/metrics/percentiles'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggTypeMetricPercentilesProvider class', function () { + + let Vis; + let indexPattern; + let aggTypeMetricPercentiles; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + aggTypeMetricPercentiles = Private(AggTypeMetricPercentilesProvider); + })); + + it('uses the custom label if it is set', function () { + const vis = new Vis(indexPattern, {}); + + // Grab the aggConfig off the vis (we don't actually use the vis for + // anything else) + const aggConfig = vis.aggs[0]; + aggConfig.params.customLabel = 'prince'; + aggConfig.params.percents = [ 95 ]; + aggConfig.params.field = { + displayName: 'bytes' + }; + + const responseAggs = aggTypeMetricPercentiles.getResponseAggs(aggConfig); + const ninetyFifthPercentileLabel = responseAggs[0].makeLabel(); + + expect(ninetyFifthPercentileLabel).to.be('95th percentile of prince'); + }); + +}); diff --git a/src/ui/public/agg_types/__tests__/metrics/std_deviation.js b/src/ui/public/agg_types/__tests__/metrics/std_deviation.js new file mode 100644 index 0000000000000..c16d9691e9744 --- /dev/null +++ b/src/ui/public/agg_types/__tests__/metrics/std_deviation.js @@ -0,0 +1,61 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypeMetricStandardDeviationProvider from 'ui/agg_types/metrics/std_deviation'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; + +describe('AggTypeMetricStandardDeviationProvider class', function () { + + let Vis; + let indexPattern; + let aggTypeMetricStandardDeviation; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + aggTypeMetricStandardDeviation = Private(AggTypeMetricStandardDeviationProvider); + })); + + it('uses the custom label if it is set', function () { + let vis = new Vis(indexPattern, {}); + + // Grab the aggConfig off the vis (we don't actually use the vis for + // anything else) + let aggConfig = vis.aggs[0]; + aggConfig.params.customLabel = 'custom label'; + aggConfig.params.field = { + displayName: 'memory' + }; + + let responseAggs = aggTypeMetricStandardDeviation.getResponseAggs(aggConfig); + let lowerStdDevLabel = responseAggs[0].makeLabel(); + let avgLabel = responseAggs[1].makeLabel(); + let upperStdDevLabel = responseAggs[2].makeLabel(); + + expect(lowerStdDevLabel).to.be('Lower custom label'); + expect(avgLabel).to.be('Average of memory'); // not expected to use custom label + expect(upperStdDevLabel).to.be('Upper custom label'); + }); + + it('uses the default labels if custom label is not set', function () { + let vis = new Vis(indexPattern, {}); + + // Grab the aggConfig off the vis (we don't actually use the vis for + // anything else) + let aggConfig = vis.aggs[0]; + aggConfig.params.field = { + displayName: 'memory' + }; + + let responseAggs = aggTypeMetricStandardDeviation.getResponseAggs(aggConfig); + let lowerStdDevLabel = responseAggs[0].makeLabel(); + let avgLabel = responseAggs[1].makeLabel(); + let upperStdDevLabel = responseAggs[2].makeLabel(); + + expect(lowerStdDevLabel).to.be('Lower Standard Deviation of memory'); + expect(avgLabel).to.be('Average of memory'); // not expected to use custom label + expect(upperStdDevLabel).to.be('Upper Standard Deviation of memory'); + }); + +}); diff --git a/src/ui/public/agg_types/__tests__/param_types/_calculate_interval.js b/src/ui/public/agg_types/__tests__/param_types/_calculate_interval.js index 5dc57a8ed33c0..47455e418a38a 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_calculate_interval.js +++ b/src/ui/public/agg_types/__tests__/param_types/_calculate_interval.js @@ -1,31 +1,35 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import VisProvider from 'ui/vis'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import AggTypesParamTypesCalculateIntervalProvider from 'ui/agg_types/param_types/_calculate_interval'; describe('calculateInterval()', function () { - var AggConfig; - var indexPattern; - var Vis; - var createFilter; - var calculateInterval; + let AggConfig; + let indexPattern; + let Vis; + let createFilter; + let calculateInterval; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(require('ui/Vis')); - AggConfig = Private(require('ui/Vis/AggConfig')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); - calculateInterval = Private(require('ui/agg_types/param_types/_calculate_interval')); + Vis = Private(VisProvider); + AggConfig = Private(VisAggConfigProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + calculateInterval = Private(AggTypesParamTypesCalculateIntervalProvider); })); - var testInterval = function (option, expected) { - var msg = 'should return ' + JSON.stringify(expected) + ' for ' + option; + let testInterval = function (option, expected) { + let msg = 'should return ' + JSON.stringify(expected) + ' for ' + option; it(msg, function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'histogram', aggs: [ { type: 'date_histogram', schema: 'segment', params: { field: '@timestamp', interval: option } } ] }); - var aggConfig = vis.aggs.byTypeName.date_histogram[0]; - var interval = calculateInterval(aggConfig); + let aggConfig = vis.aggs.byTypeName.date_histogram[0]; + let interval = calculateInterval(aggConfig); _.each(expected, function (val, key) { expect(interval).to.have.property(key, val); }); diff --git a/src/ui/public/agg_types/__tests__/param_types/_field.js b/src/ui/public/agg_types/__tests__/param_types/_field.js index 2bc90026efba4..cc2679fc0d4b8 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_field.js +++ b/src/ui/public/agg_types/__tests__/param_types/_field.js @@ -1,21 +1,23 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesFieldProvider from 'ui/agg_types/param_types/field'; describe('Field', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var BaseAggParam; - var FieldAggParam; + let BaseAggParam; + let FieldAggParam; beforeEach(ngMock.module('kibana')); // fetch out deps beforeEach(ngMock.inject(function (Private) { - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - FieldAggParam = Private(require('ui/agg_types/param_types/field')); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + FieldAggParam = Private(AggTypesParamTypesFieldProvider); })); describe('constructor', function () { it('it is an instance of BaseAggParam', function () { - var aggParam = new FieldAggParam({ + let aggParam = new FieldAggParam({ name: 'field' }); diff --git a/src/ui/public/agg_types/__tests__/param_types/_optioned.js b/src/ui/public/agg_types/__tests__/param_types/_optioned.js index b15ecd02e47bd..69165309fb12d 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_optioned.js +++ b/src/ui/public/agg_types/__tests__/param_types/_optioned.js @@ -1,21 +1,23 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesOptionedProvider from 'ui/agg_types/param_types/optioned'; describe('Optioned', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var BaseAggParam; - var OptionedAggParam; + let BaseAggParam; + let OptionedAggParam; beforeEach(ngMock.module('kibana')); // fetch out deps beforeEach(ngMock.inject(function (Private) { - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - OptionedAggParam = Private(require('ui/agg_types/param_types/optioned')); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + OptionedAggParam = Private(AggTypesParamTypesOptionedProvider); })); describe('constructor', function () { it('it is an instance of BaseAggParam', function () { - var aggParam = new OptionedAggParam({ + let aggParam = new OptionedAggParam({ name: 'some_param', type: 'optioned' }); diff --git a/src/ui/public/agg_types/__tests__/param_types/_raw_json.js b/src/ui/public/agg_types/__tests__/param_types/_raw_json.js index 31033b3080d46..86c69ee620dad 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_raw_json.js +++ b/src/ui/public/agg_types/__tests__/param_types/_raw_json.js @@ -1,19 +1,21 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesRawJsonProvider from 'ui/agg_types/param_types/raw_json'; module.exports = describe('JSON', function () { - var paramName = 'json_test'; - var BaseAggParam; - var JsonAggParam; - var aggParam; - var aggConfig; - var output; + let paramName = 'json_test'; + let BaseAggParam; + let JsonAggParam; + let aggParam; + let aggConfig; + let output; function initAggParam(config) { config = config || {}; - var defaults = { + let defaults = { name: paramName, type: 'json' }; @@ -28,8 +30,8 @@ module.exports = describe('JSON', function () { aggConfig = { params: {} }; output = { params: {} }; - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - JsonAggParam = Private(require('ui/agg_types/param_types/raw_json')); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + JsonAggParam = Private(AggTypesParamTypesRawJsonProvider); initAggParam(); })); @@ -57,7 +59,7 @@ module.exports = describe('JSON', function () { }); it('should append param when valid JSON', function () { - var jsonData = JSON.stringify({ + let jsonData = JSON.stringify({ new_param: 'should exist in output' }); @@ -73,7 +75,7 @@ module.exports = describe('JSON', function () { }); it('should not overwrite existing params', function () { - var jsonData = JSON.stringify({ + let jsonData = JSON.stringify({ new_param: 'should exist in output', existing: 'should be used' }); @@ -86,7 +88,7 @@ module.exports = describe('JSON', function () { }); it('should drop nulled params', function () { - var jsonData = JSON.stringify({ + let jsonData = JSON.stringify({ new_param: 'should exist in output', field: null }); diff --git a/src/ui/public/agg_types/__tests__/param_types/_regex.js b/src/ui/public/agg_types/__tests__/param_types/_regex.js index 6d466df588925..a29c7b7d812e3 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_regex.js +++ b/src/ui/public/agg_types/__tests__/param_types/_regex.js @@ -1,25 +1,29 @@ +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesRegexProvider from 'ui/agg_types/param_types/regex'; +import VisProvider from 'ui/vis'; +import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('Regex', function () { - var _ = require('lodash'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var BaseAggParam; - var RegexAggParam; - var Vis; - var indexPattern; + let BaseAggParam; + let RegexAggParam; + let Vis; + let indexPattern; beforeEach(ngMock.module('kibana')); // fetch out deps beforeEach(ngMock.inject(function (Private) { - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - RegexAggParam = Private(require('ui/agg_types/param_types/regex')); - Vis = Private(require('ui/Vis')); - indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern')); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + RegexAggParam = Private(AggTypesParamTypesRegexProvider); + Vis = Private(VisProvider); + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); describe('constructor', function () { it('should be an instance of BaseAggParam', function () { - var aggParam = new RegexAggParam({ + let aggParam = new RegexAggParam({ name: 'some_param', type: 'regex' }); @@ -30,13 +34,13 @@ describe('Regex', function () { }); describe('write results', function () { - var aggParam; - var aggConfig; - var output = { params: {} }; - var paramName = 'exclude'; + let aggParam; + let aggConfig; + let output = { params: {} }; + let paramName = 'exclude'; beforeEach(function () { - var vis = new Vis(indexPattern, { + let vis = new Vis(indexPattern, { type: 'pie', aggs: [ { type: 'terms', schema: 'split', params: { field: 'extension' }}, @@ -69,20 +73,6 @@ describe('Regex', function () { aggParam.write(aggConfig, output); expect(output.params).to.have.property(paramName); expect(output.params[paramName]).to.eql({ pattern: 'testing' }); - expect(output.params[paramName]).not.to.have.property('flags'); - }); - - it('should include flags', function () { - aggConfig.params[paramName] = { - pattern: 'testing', - flags: [ 'TEST1', 'TEST2', 'TEST_RED', 'TEST_BLUE' ] - }; - - aggParam.write(aggConfig, output); - expect(output.params).to.have.property(paramName); - expect(output.params[paramName]).to.have.property('flags'); - expect(typeof output.params[paramName].flags).to.be('string'); - expect(output.params[paramName].flags).to.be('TEST1|TEST2|TEST_RED|TEST_BLUE'); }); }); }); diff --git a/src/ui/public/agg_types/__tests__/param_types/_string.js b/src/ui/public/agg_types/__tests__/param_types/_string.js index f55a0a207b7c0..fde04e53a2742 100644 --- a/src/ui/public/agg_types/__tests__/param_types/_string.js +++ b/src/ui/public/agg_types/__tests__/param_types/_string.js @@ -1,18 +1,20 @@ -var _ = require('lodash'); -var expect = require('expect.js'); -var ngMock = require('ngMock'); +import _ from 'lodash'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +import AggTypesParamTypesStringProvider from 'ui/agg_types/param_types/string'; module.exports = describe('String', function () { - var paramName = 'json_test'; - var BaseAggParam; - var StringAggParam; - var aggParam; - var aggConfig; - var output; + let paramName = 'json_test'; + let BaseAggParam; + let StringAggParam; + let aggParam; + let aggConfig; + let output; function initAggParam(config) { config = config || {}; - var defaults = { + let defaults = { name: paramName, type: 'string' }; @@ -24,8 +26,8 @@ module.exports = describe('String', function () { // fetch our deps beforeEach(ngMock.inject(function (Private) { - BaseAggParam = Private(require('ui/agg_types/param_types/base')); - StringAggParam = Private(require('ui/agg_types/param_types/string')); + BaseAggParam = Private(AggTypesParamTypesBaseProvider); + StringAggParam = Private(AggTypesParamTypesStringProvider); aggConfig = { params: {} }; output = { params: {} }; @@ -40,8 +42,8 @@ module.exports = describe('String', function () { describe('write', function () { it('should append param by name', function () { - var paramName = 'testing'; - var params = {}; + let paramName = 'testing'; + let params = {}; params[paramName] = 'some input'; initAggParam({ name: paramName }); @@ -53,8 +55,8 @@ module.exports = describe('String', function () { }); it('should not be in output with empty input', function () { - var paramName = 'more_testing'; - var params = {}; + let paramName = 'more_testing'; + let params = {}; params[paramName] = ''; initAggParam({ name: paramName }); diff --git a/src/ui/public/agg_types/__tests__/param_types/index.js b/src/ui/public/agg_types/__tests__/param_types/index.js index ede5c6f4966e5..d2e9c6e07d9ea 100644 --- a/src/ui/public/agg_types/__tests__/param_types/index.js +++ b/src/ui/public/agg_types/__tests__/param_types/index.js @@ -1,7 +1,7 @@ +import './_field'; +import './_optioned'; +import './_regex'; +import './_string'; +import './_raw_json'; describe('ParamTypes', function () { - require('./_field'); - require('./_optioned'); - require('./_regex'); - require('./_string'); - require('./_raw_json'); }); diff --git a/src/ui/public/agg_types/__tests__/utils/_stub_agg_params.js b/src/ui/public/agg_types/__tests__/utils/_stub_agg_params.js index 7848556f10aef..c12ced529cf5f 100644 --- a/src/ui/public/agg_types/__tests__/utils/_stub_agg_params.js +++ b/src/ui/public/agg_types/__tests__/utils/_stub_agg_params.js @@ -1,8 +1,8 @@ -var _ = require('lodash'); -var sinon = require('auto-release-sinon'); +import _ from 'lodash'; +import sinon from 'auto-release-sinon'; function ParamClassStub(parent, body) { - var stub = sinon.spy(body || function () { + let stub = sinon.spy(body || function () { stub.Super && stub.Super.call(this); }); if (parent) _.class(stub).inherits(parent); @@ -14,7 +14,7 @@ function ParamClassStub(parent, body) { * This method should be passed directly to ngMock.inject(); * * ```js - * var stubParamClasses = require('./utils/_stub_agg_params'); + * let stubParamClasses = require('./utils/_stub_agg_params'); * describe('something', function () { * beforeEach(ngMock.inject(stubParamClasses)); * }) @@ -24,7 +24,7 @@ function ParamClassStub(parent, body) { * @return {undefined} */ module.exports = function stubParamClasses(Private) { - var BaseAggParam = Private.stub( + let BaseAggParam = Private.stub( require('ui/agg_types/param_types/base'), new ParamClassStub(null, function (config) { _.assign(this, config); diff --git a/src/ui/public/agg_types/agg_params.js b/src/ui/public/agg_types/agg_params.js new file mode 100644 index 0000000000000..d6c435c214b4c --- /dev/null +++ b/src/ui/public/agg_types/agg_params.js @@ -0,0 +1,77 @@ +import 'ui/filters/label'; +import _ from 'lodash'; +import IndexedArray from 'ui/indexed_array'; +import AggTypesParamTypesFieldProvider from 'ui/agg_types/param_types/field'; +import AggTypesParamTypesOptionedProvider from 'ui/agg_types/param_types/optioned'; +import AggTypesParamTypesRegexProvider from 'ui/agg_types/param_types/regex'; +import AggTypesParamTypesStringProvider from 'ui/agg_types/param_types/string'; +import AggTypesParamTypesRawJsonProvider from 'ui/agg_types/param_types/raw_json'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +export default function AggParamsFactory(Private) { + + + let paramTypeMap = { + field: Private(AggTypesParamTypesFieldProvider), + optioned: Private(AggTypesParamTypesOptionedProvider), + regex: Private(AggTypesParamTypesRegexProvider), + string: Private(AggTypesParamTypesStringProvider), + json: Private(AggTypesParamTypesRawJsonProvider), + _default: Private(AggTypesParamTypesBaseProvider) + }; + + /** + * Wraps a list of {{#crossLink "AggParam"}}{{/crossLink}} objects; owned by an {{#crossLink "AggType"}}{{/crossLink}} + * + * used to create: + * - `FieldAggParam` – When the config has `name: "field"` + * - `*AggParam` – When the type matches something in the map above + * - `BaseAggParam` – All other params + * + * @class AggParams + * @constructor + * @extends IndexedArray + * @param {object[]} params - array of params that get new-ed up as AggParam objects as descibed above + */ + _.class(AggParams).inherits(IndexedArray); + function AggParams(params) { + AggParams.Super.call(this, { + index: ['name'], + initialSet: params.map(function (config) { + let type = config.name === 'field' ? config.name : config.type; + let Class = paramTypeMap[type] || paramTypeMap._default; + return new Class(config); + }) + }); + } + + /** + * Reads an aggConfigs + * + * @method write + * @param {AggConfig} aggConfig + * the AggConfig object who's type owns these aggParams and contains the param values for our param defs + * @param {object} [locals] + * an array of locals that will be available to the write function (can be used to enhance + * the quality of things like date_histogram's "auto" interval) + * @return {object} output + * output of the write calls, reduced into a single object. A `params: {}` property is exposed on the + * output object which is used to create the agg dsl for the search request. All other properties + * are dependent on the AggParam#write methods which should be studied for each AggType. + */ + AggParams.prototype.write = function (aggConfig, locals) { + let output = { params: {} }; + locals = locals || {}; + + this.forEach(function (param) { + if (param.write) { + param.write(aggConfig, output, locals); + } else { + output.params[param.name] = aggConfig.params[param.name]; + } + }); + + return output; + }; + + return AggParams; +}; diff --git a/src/ui/public/agg_types/agg_type.js b/src/ui/public/agg_types/agg_type.js new file mode 100644 index 0000000000000..64f9db58e33e9 --- /dev/null +++ b/src/ui/public/agg_types/agg_type.js @@ -0,0 +1,150 @@ +import _ from 'lodash'; +import AggTypesAggParamsProvider from 'ui/agg_types/agg_params'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +export default function AggTypeFactory(Private) { + let AggParams = Private(AggTypesAggParamsProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); + + /** + * Generic AggType Constructor + * + * Used to create the values exposed by the agg_types module. + * + * @class AggType + * @private + * @param {object} config - used to set the properties of the AggType + */ + function AggType(config) { + + /** + * the unique, unchanging, name that we have assigned this aggType + * + * @property name + * @type {string} + */ + this.name = config.name; + + /** + * the name of the elasticsearch aggregation that this aggType represents. Usually just this.name + * + * @property name + * @type {string} + */ + this.dslName = config.dslName || config.name; + + /** + * the user friendly name that will be shown in the ui for this aggType + * + * @property title + * @type {string} + */ + this.title = config.title; + + /** + * a function that will be called when this aggType is assigned to + * an aggConfig, and that aggConfig is being rendered (in a form, chart, etc.). + * + * @method makeLabel + * @param {AggConfig} aggConfig - an agg config of this type + * @returns {string} - label that can be used in the ui to descripe the aggConfig + */ + this.makeLabel = config.makeLabel || _.constant(this.name); + + /** + * Describes if this aggType creates data that is ordered, and if that ordered data + * is some sort of time series. + * + * If the aggType does not create ordered data, set this to something "falsey". + * + * If this does create orderedData, then the value should be an object. + * + * If the orderdata is some sort of time series, `this.ordered` should be an object + * with the property `date: true` + * + * @property ordered + * @type {object|undefined} + */ + this.ordered = config.ordered; + + /** + * Flag that prevents this aggregation from being included in the dsl. This is only + * used by the count aggregation (currently) since it doesn't really exist and it's output + * is available on every bucket. + * + * @type {Boolean} + */ + this.hasNoDsl = !!config.hasNoDsl; + + /** + * The method to create a filter representation of the bucket + * @param {object} aggConfig The instance of the aggConfig + * @param {mixed} key The key for the bucket + * @returns {object} The filter + */ + this.createFilter = config.createFilter; + + /** + * An instance of {{#crossLink "AggParams"}}{{/crossLink}}. + * + * @property params + * @type {AggParams} + */ + this.params = config.params || []; + if (!(this.params instanceof AggParams)) { + // always append the raw JSON param + this.params.push({ + name: 'json', + type: 'json', + advanced: true + }); + // always append custom label + + if (config.customLabels !== false) { + this.params.push({ + name: 'customLabel', + type: 'string', + write: _.noop + }); + } + + this.params = new AggParams(this.params); + } + + /** + * Designed for multi-value metric aggs, this method can return a + * set of AggConfigs that should replace this aggConfig in result sets + * that walk the AggConfig set. + * + * @method getResponseAggs + * @returns {array[AggConfig]|undefined} - an array of aggConfig objects + * that should replace this one, + * or undefined + */ + this.getResponseAggs = config.getResponseAggs || _.noop; + + /** + * A function that will be called each time an aggConfig of this type + * is created, giving the agg type a chance to modify the agg config + */ + this.decorateAggConfig = config.decorateAggConfig || null; + + if (config.getFormat) { + this.getFormat = config.getFormat; + } + } + + /** + * 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} + */ + AggType.prototype.getFormat = function (agg) { + let field = agg.field(); + return field ? field.format : fieldFormats.getDefaultInstance('string'); + }; + + return AggType; +}; diff --git a/src/ui/public/agg_types/buckets/RangeKey.js b/src/ui/public/agg_types/buckets/RangeKey.js deleted file mode 100644 index 5917de5ce335a..0000000000000 --- a/src/ui/public/agg_types/buckets/RangeKey.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = function () { - - const id = Symbol('id'); - - class RangeKey { - constructor(bucket) { - this.gte = bucket.from == null ? -Infinity : bucket.from; - this.lt = bucket.to == null ? +Infinity : bucket.to; - - this[id] = RangeKey.idBucket(bucket); - } - - - static idBucket(bucket) { - return `from:${bucket.from},to:${bucket.to}`; - } - - toString() { - return this[id]; - } - } - - - return RangeKey; -}; diff --git a/src/ui/public/agg_types/buckets/_bucket_agg_type.js b/src/ui/public/agg_types/buckets/_bucket_agg_type.js index b76e3911e0c8e..4249ec29be71e 100644 --- a/src/ui/public/agg_types/buckets/_bucket_agg_type.js +++ b/src/ui/public/agg_types/buckets/_bucket_agg_type.js @@ -1,21 +1,20 @@ -define(function (require) { - return function BucketAggTypeProvider(Private) { - var _ = require('lodash'); - var AggType = Private(require('ui/agg_types/AggType')); +import _ from 'lodash'; +import AggTypesAggTypeProvider from 'ui/agg_types/agg_type'; +export default function BucketAggTypeProvider(Private) { + let AggType = Private(AggTypesAggTypeProvider); - _.class(BucketAggType).inherits(AggType); - function BucketAggType(config) { - BucketAggType.Super.call(this, config); + _.class(BucketAggType).inherits(AggType); + function BucketAggType(config) { + BucketAggType.Super.call(this, config); - if (_.isFunction(config.getKey)) { - this.getKey = config.getKey; - } + if (_.isFunction(config.getKey)) { + this.getKey = config.getKey; } + } - BucketAggType.prototype.getKey = function (bucket, key) { - return key || bucket.key; - }; - - return BucketAggType; + BucketAggType.prototype.getKey = function (bucket, key) { + return key || bucket.key; }; -}); + + return BucketAggType; +}; diff --git a/src/ui/public/agg_types/buckets/_bucket_count_between.js b/src/ui/public/agg_types/buckets/_bucket_count_between.js index a969011a9cfec..3fbcd3e723343 100644 --- a/src/ui/public/agg_types/buckets/_bucket_count_between.js +++ b/src/ui/public/agg_types/buckets/_bucket_count_between.js @@ -1,44 +1,42 @@ -define(function (require) { - return function BucketCountBetweenProvider() { +export default function BucketCountBetweenProvider() { - /** - * Count the number of bucket aggs between two agg config objects owned - * by the same vis. - * - * If one of the two aggs was not found in the agg list, returns null. - * If a was found after b, the count will be negative - * If a was found first, the count will be positive. - * - * @param {AggConfig} aggConfigA - the aggConfig that is expected first - * @param {AggConfig} aggConfigB - the aggConfig that is expected second - * @return {null|number} - */ - function bucketCountBetween(aggConfigA, aggConfigB) { - var aggs = aggConfigA.vis.aggs.getRequestAggs(); + /** + * Count the number of bucket aggs between two agg config objects owned + * by the same vis. + * + * If one of the two aggs was not found in the agg list, returns null. + * If a was found after b, the count will be negative + * If a was found first, the count will be positive. + * + * @param {AggConfig} aggConfigA - the aggConfig that is expected first + * @param {AggConfig} aggConfigB - the aggConfig that is expected second + * @return {null|number} + */ + function bucketCountBetween(aggConfigA, aggConfigB) { + let aggs = aggConfigA.vis.aggs.getRequestAggs(); - var aIndex = aggs.indexOf(aggConfigA); - var bIndex = aggs.indexOf(aggConfigB); + let aIndex = aggs.indexOf(aggConfigA); + let bIndex = aggs.indexOf(aggConfigB); - if (aIndex === -1 || bIndex === -1) { - return null; - } + if (aIndex === -1 || bIndex === -1) { + return null; + } - // return a negative distance, if b is before a - var negative = (aIndex > bIndex); + // return a negative distance, if b is before a + let negative = (aIndex > bIndex); - var count = aggs - .slice(Math.min(aIndex, bIndex), Math.max(aIndex, bIndex)) - .reduce(function (count, cfg) { - if (cfg === aggConfigA || cfg === aggConfigB || cfg.schema.group !== 'buckets') { - return count; - } else { - return count + 1; - } - }, 0); + let count = aggs + .slice(Math.min(aIndex, bIndex), Math.max(aIndex, bIndex)) + .reduce(function (count, cfg) { + if (cfg === aggConfigA || cfg === aggConfigB || cfg.schema.group !== 'buckets') { + return count; + } else { + return count + 1; + } + }, 0); - return (negative ? -1 : 1) * count; - } + return (negative ? -1 : 1) * count; + } - return bucketCountBetween; - }; -}); + return bucketCountBetween; +}; diff --git a/src/ui/public/agg_types/buckets/_interval_options.js b/src/ui/public/agg_types/buckets/_interval_options.js index fa27d2291a01d..f1a8f151898d2 100644 --- a/src/ui/public/agg_types/buckets/_interval_options.js +++ b/src/ui/public/agg_types/buckets/_interval_options.js @@ -1,53 +1,55 @@ -define(function (require) { - return function IntervalOptionsService(Private) { - var moment = require('moment'); - require('ui/directives/input_whole_number'); +import moment from 'moment'; +import 'ui/directives/input_whole_number'; +export default function IntervalOptionsService(Private) { - // shorthand - var ms = function (type) { return moment.duration(1, type).asMilliseconds(); }; + // shorthand + let ms = function (type) { return moment.duration(1, type).asMilliseconds(); }; - return [ - { - display: 'Auto', - val: 'auto', - enabled: function (agg) { - // not only do we need a time field, but the selected field needs - // to be the time field. (see #3028) - return agg.fieldIsTimeField(); - } - }, - { - display: 'Second', - val: 's' - }, - { - display: 'Minute', - val: 'm' - }, - { - display: 'Hourly', - val: 'h' - }, - { - display: 'Daily', - val: 'd' - }, - { - display: 'Weekly', - val: 'w' - }, - { - display: 'Monthly', - val: 'M' - }, - { - display: 'Yearly', - val: 'y' - }, - { - display: 'Custom', - val: 'custom' + return [ + { + display: 'Auto', + val: 'auto', + enabled: function (agg) { + // not only do we need a time field, but the selected field needs + // to be the time field. (see #3028) + return agg.fieldIsTimeField(); } - ]; - }; -}); + }, + { + display: 'Millisecond', + val: 'ms' + }, + { + display: 'Second', + val: 's' + }, + { + display: 'Minute', + val: 'm' + }, + { + display: 'Hourly', + val: 'h' + }, + { + display: 'Daily', + val: 'd' + }, + { + display: 'Weekly', + val: 'w' + }, + { + display: 'Monthly', + val: 'M' + }, + { + display: 'Yearly', + val: 'y' + }, + { + display: 'Custom', + val: 'custom' + } + ]; +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/date_histogram.js b/src/ui/public/agg_types/buckets/create_filter/date_histogram.js index 6040c31ac207b..67b23bbf2e945 100644 --- a/src/ui/public/agg_types/buckets/create_filter/date_histogram.js +++ b/src/ui/public/agg_types/buckets/create_filter/date_histogram.js @@ -1,17 +1,16 @@ -define(function (require) { - return function createDateHistogramFilterProvider(Private) { - var moment = require('moment'); - var buildRangeFilter = require('ui/filter_manager/lib/range'); +import moment from 'moment'; +import buildRangeFilter from 'ui/filter_manager/lib/range'; +export default function createDateHistogramFilterProvider(Private) { - return function (agg, key) { - var start = moment(key); - var interval = agg.buckets.getInterval(); - - return buildRangeFilter(agg.params.field, { - gte: start.valueOf(), - lte: start.add(interval).subtract(1, 'ms').valueOf() - }, agg.vis.indexPattern); - }; + return function (agg, key) { + let start = moment(key); + let interval = agg.buckets.getInterval(); + return buildRangeFilter(agg.params.field, { + gte: start.valueOf(), + lt: start.add(interval).valueOf(), + format: 'epoch_millis' + }, agg.vis.indexPattern); }; -}); + +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/date_range.js b/src/ui/public/agg_types/buckets/create_filter/date_range.js index 9b075ee023f03..89e8232b6e53e 100644 --- a/src/ui/public/agg_types/buckets/create_filter/date_range.js +++ b/src/ui/public/agg_types/buckets/create_filter/date_range.js @@ -1,19 +1,17 @@ -define(function (require) { - var dateRange = require('ui/utils/date_range'); +import dateRange from 'ui/utils/date_range'; +import buildRangeFilter from 'ui/filter_manager/lib/range'; - return function createDateRangeFilterProvider(config) { - var buildRangeFilter = require('ui/filter_manager/lib/range'); +export default function createDateRangeFilterProvider(config) { - return function (agg, key) { - var range = dateRange.parse(key, config.get('dateFormat')); + return function (agg, key) { + let range = dateRange.parse(key, config.get('dateFormat')); - var filter = {}; - if (range.from) filter.gte = +range.from; - if (range.to) filter.lt = +range.to; - if (range.to && range.from) filter.format = 'epoch_millis'; - - return buildRangeFilter(agg.params.field, filter, agg.vis.indexPattern); - }; + let filter = {}; + if (range.from) filter.gte = +range.from; + if (range.to) filter.lt = +range.to; + if (range.to && range.from) filter.format = 'epoch_millis'; + return buildRangeFilter(agg.params.field, filter, agg.vis.indexPattern); }; -}); + +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/filters.js b/src/ui/public/agg_types/buckets/create_filter/filters.js index 89b22f6adc06c..e9cbde979f2f8 100644 --- a/src/ui/public/agg_types/buckets/create_filter/filters.js +++ b/src/ui/public/agg_types/buckets/create_filter/filters.js @@ -1,15 +1,13 @@ -define(function (require) { - var buildQueryFilter = require('ui/filter_manager/lib/query'); - var _ = require('lodash'); - return function CreateFilterFiltersProvider(Private) { - return function (aggConfig, key) { - // have the aggConfig write agg dsl params - var dslFilters = _.get(aggConfig.toDsl(), 'filters.filters'); - var filter = dslFilters[key]; +import buildQueryFilter from 'ui/filter_manager/lib/query'; +import _ from 'lodash'; +export default function CreateFilterFiltersProvider(Private) { + return function (aggConfig, key) { + // have the aggConfig write agg dsl params + let dslFilters = _.get(aggConfig.toDsl(), 'filters.filters'); + let filter = dslFilters[key]; - if (filter) { - return buildQueryFilter(filter.query, aggConfig.vis.indexPattern.id); - } - }; + if (filter) { + return buildQueryFilter(filter.query, aggConfig.vis.indexPattern.id); + } }; -}); +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/histogram.js b/src/ui/public/agg_types/buckets/create_filter/histogram.js index c84883a8fd5b4..fd6c8c1e21c16 100644 --- a/src/ui/public/agg_types/buckets/create_filter/histogram.js +++ b/src/ui/public/agg_types/buckets/create_filter/histogram.js @@ -1,16 +1,14 @@ -define(function (require) { - var buildRangeFilter = require('ui/filter_manager/lib/range'); +import buildRangeFilter from 'ui/filter_manager/lib/range'; - return function createHistogramFitlerProvider(Private) { - return function (aggConfig, key) { - var value = parseInt(key, 10); +export default function createHistogramFilterProvider(Private) { + return function (aggConfig, key) { + let value = parseInt(key, 10); - return buildRangeFilter( - aggConfig.params.field, - {gte: value, lt: value + aggConfig.params.interval}, - aggConfig.vis.indexPattern, - aggConfig.fieldFormatter()(key) - ); - }; + return buildRangeFilter( + aggConfig.params.field, + {gte: value, lt: value + aggConfig.params.interval}, + aggConfig.vis.indexPattern, + aggConfig.fieldFormatter()(key) + ); }; -}); +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/ip_range.js b/src/ui/public/agg_types/buckets/create_filter/ip_range.js index ccb21d517f4c9..ea5efa64adc34 100644 --- a/src/ui/public/agg_types/buckets/create_filter/ip_range.js +++ b/src/ui/public/agg_types/buckets/create_filter/ip_range.js @@ -1,20 +1,18 @@ -define(function (require) { - var CidrMask = require('ui/utils/CidrMask'); - var buildRangeFilter = require('ui/filter_manager/lib/range'); - return function createIpRangeFilterProvider() { - return function (aggConfig, key) { - var range; - if (aggConfig.params.ipRangeType === 'mask') { - range = new CidrMask(key).getRange(); - } else { - var addresses = key.split(/\-/); - range = { - from: addresses[0], - to: addresses[1] - }; - } +import CidrMask from 'ui/utils/cidr_mask'; +import buildRangeFilter from 'ui/filter_manager/lib/range'; +export default function createIpRangeFilterProvider() { + return function (aggConfig, key) { + let range; + if (aggConfig.params.ipRangeType === 'mask') { + range = new CidrMask(key).getRange(); + } else { + let addresses = key.split(/\-/); + range = { + from: addresses[0], + to: addresses[1] + }; + } - return buildRangeFilter(aggConfig.params.field, {gte: range.from, lte: range.to}, aggConfig.vis.indexPattern); - }; + return buildRangeFilter(aggConfig.params.field, {gte: range.from, lte: range.to}, aggConfig.vis.indexPattern); }; -}); +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/range.js b/src/ui/public/agg_types/buckets/create_filter/range.js index 70af0a2c19097..a64a31dd4fe84 100644 --- a/src/ui/public/agg_types/buckets/create_filter/range.js +++ b/src/ui/public/agg_types/buckets/create_filter/range.js @@ -1,13 +1,11 @@ -define(function (require) { - var buildRangeFilter = require('ui/filter_manager/lib/range'); - return function createRangeFilterProvider(Private) { - return function (aggConfig, key) { - return buildRangeFilter( - aggConfig.params.field, - key, - aggConfig.vis.indexPattern, - aggConfig.fieldFormatter()(key) - ); - }; +import buildRangeFilter from 'ui/filter_manager/lib/range'; +export default function createRangeFilterProvider(Private) { + return function (aggConfig, key) { + return buildRangeFilter( + aggConfig.params.field, + key, + aggConfig.vis.indexPattern, + aggConfig.fieldFormatter()(key) + ); }; -}); +}; diff --git a/src/ui/public/agg_types/buckets/create_filter/terms.js b/src/ui/public/agg_types/buckets/create_filter/terms.js index db2361bffe2f3..c3a6774e3682e 100644 --- a/src/ui/public/agg_types/buckets/create_filter/terms.js +++ b/src/ui/public/agg_types/buckets/create_filter/terms.js @@ -1,8 +1,6 @@ -define(function (require) { - var buildPhraseFilter = require('ui/filter_manager/lib/phrase'); - return function createTermsFilterProvider(Private) { - return function (aggConfig, key) { - return buildPhraseFilter(aggConfig.params.field, key, aggConfig.vis.indexPattern); - }; +import buildPhraseFilter from 'ui/filter_manager/lib/phrase'; +export default function createTermsFilterProvider(Private) { + return function (aggConfig, key) { + return buildPhraseFilter(aggConfig.params.field, key, aggConfig.vis.indexPattern); }; -}); +}; diff --git a/src/ui/public/agg_types/buckets/date_histogram.js b/src/ui/public/agg_types/buckets/date_histogram.js index a323e304850c5..58d3f0eb144a8 100644 --- a/src/ui/public/agg_types/buckets/date_histogram.js +++ b/src/ui/public/agg_types/buckets/date_histogram.js @@ -1,161 +1,158 @@ -define(function (require) { - return function DateHistogramAggType(timefilter, config, Private) { - var _ = require('lodash'); - var moment = require('moment'); - var tzDetect = require('jstimezonedetect').jstz; - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var TimeBuckets = Private(require('ui/time_buckets')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/date_histogram')); - var intervalOptions = Private(require('ui/agg_types/buckets/_interval_options')); - var configDefaults = Private(require('ui/config/defaults')); - - var detectedTimezone = tzDetect.determine().name(); - var tzOffset = moment().format('Z'); - - function getInterval(agg) { - var interval = _.get(agg, ['params', 'interval']); - if (interval && interval.val === 'custom') interval = _.get(agg, ['params', 'customInterval']); - return interval; +import { jstz as tzDetect } from 'jstimezonedetect'; +import _ from 'lodash'; +import moment from 'moment'; +import 'ui/filters/field_type'; +import 'ui/validate_date_interval'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import TimeBucketsProvider from 'ui/time_buckets'; +import AggTypesBucketsCreateFilterDateHistogramProvider from 'ui/agg_types/buckets/create_filter/date_histogram'; +import AggTypesBucketsIntervalOptionsProvider from 'ui/agg_types/buckets/_interval_options'; +import intervalTemplate from 'ui/agg_types/controls/interval.html'; +export default function DateHistogramAggType(timefilter, config, Private) { + const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + const TimeBuckets = Private(TimeBucketsProvider); + const createFilter = Private(AggTypesBucketsCreateFilterDateHistogramProvider); + const intervalOptions = Private(AggTypesBucketsIntervalOptionsProvider); + + const detectedTimezone = tzDetect.determine().name(); + const tzOffset = moment().format('Z'); + + function getInterval(agg) { + const interval = _.get(agg, ['params', 'interval']); + if (interval && interval.val === 'custom') { + return _.get(agg, ['params', 'customInterval']); } - - function setBounds(agg, force) { - if (agg.buckets._alreadySet && !force) return; - agg.buckets._alreadySet = true; - agg.buckets.setBounds(agg.fieldIsTimeField() && timefilter.getActiveBounds()); - } - - require('ui/filters/field_type'); - require('ui/validateDateInterval'); - - return new BucketAggType({ - name: 'date_histogram', - title: 'Date Histogram', - ordered: { - date: true - }, - makeLabel: function (agg) { - var output = this.params.write(agg); - var params = output.params; - return params.field + ' per ' + (output.metricScaleText || output.bucketInterval.description); - }, - createFilter: createFilter, - decorateAggConfig: function () { - var buckets; - return { - buckets: { - configurable: true, - get: function () { - if (buckets) return buckets; - - buckets = new TimeBuckets(); - buckets.setInterval(getInterval(this)); - setBounds(this); - - return buckets; - } + return interval; + } + + function setBounds(agg, force) { + if (agg.buckets._alreadySet && !force) return; + agg.buckets._alreadySet = true; + agg.buckets.setBounds(agg.fieldIsTimeField() && timefilter.getActiveBounds()); + } + + + return new BucketAggType({ + name: 'date_histogram', + title: 'Date Histogram', + ordered: { + date: true + }, + makeLabel: function (agg) { + const output = this.params.write(agg); + const params = output.params; + return params.field + ' per ' + (output.metricScaleText || output.bucketInterval.description); + }, + createFilter: createFilter, + decorateAggConfig: function () { + let buckets; + return { + buckets: { + configurable: true, + get: function () { + if (buckets) return buckets; + + buckets = new TimeBuckets(); + buckets.setInterval(getInterval(this)); + setBounds(this); + + return buckets; } - }; + } + }; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'date', + default: function (agg) { + return agg.vis.indexPattern.timeFieldName; + }, + onChange: function (agg) { + if (_.get(agg, 'params.interval.val') === 'auto' && !agg.fieldIsTimeField()) { + delete agg.params.interval; + } + + setBounds(agg, true); + } }, - params: [ - { - name: 'field', - filterFieldTypes: 'date', - default: function (agg) { - return agg.vis.indexPattern.timeFieldName; - }, - onChange: function (agg) { - if (_.get(agg, 'params.interval.val') === 'auto' && !agg.fieldIsTimeField()) { - delete agg.params.interval; - } - setBounds(agg, true); - } + { + name: 'interval', + type: 'optioned', + deserialize: function (state) { + const interval = _.find(intervalOptions, {val: state}); + return interval || _.find(intervalOptions, function (option) { + // For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year', + // but this maps the old values to the new values + return Number(moment.duration(1, state)) === Number(moment.duration(1, option.val)); + }); + }, + default: 'auto', + options: intervalOptions, + editor: intervalTemplate, + onRequest: function (agg) { + setBounds(agg, true); }, + write: function (agg, output) { + setBounds(agg); + agg.buckets.setInterval(getInterval(agg)); + + const interval = agg.buckets.getInterval(); + output.bucketInterval = interval; + output.params.interval = interval.expression; + + const isDefaultTimezone = config.isDefault('dateFormat:tz'); + if (isDefaultTimezone) { + output.params.time_zone = detectedTimezone || tzOffset; + } else { + output.params.time_zone = config.get('dateFormat:tz'); + } - { - name: 'interval', - type: 'optioned', - deserialize: function (state) { - var interval = _.find(intervalOptions, {val: state}); - return interval || _.find(intervalOptions, function (option) { - // For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year', - // but this maps the old values to the new values - return Number(moment.duration(1, state)) === Number(moment.duration(1, option.val)); + const scaleMetrics = interval.scaled && interval.scale < 1; + if (scaleMetrics) { + const all = _.every(agg.vis.aggs.bySchemaGroup.metrics, function (agg) { + return agg.type && (agg.type.name === 'count' || agg.type.name === 'sum'); }); - }, - default: 'auto', - options: intervalOptions, - editor: require('ui/agg_types/controls/interval.html'), - onRequest: function (agg) { - setBounds(agg, true); - }, - write: function (agg, output) { - setBounds(agg); - agg.buckets.setInterval(getInterval(agg)); - - var interval = agg.buckets.getInterval(); - output.bucketInterval = interval; - output.params.interval = interval.expression; - - var isDefaultTimezone = config.get('dateFormat:tz') === configDefaults['dateFormat:tz'].value; - output.params.time_zone = isDefaultTimezone ? - (detectedTimezone || tzOffset) : - config.get('dateFormat:tz'); - - var scaleMetrics = interval.scaled && interval.scale < 1; - if (scaleMetrics) { - scaleMetrics = _.every(agg.vis.aggs.bySchemaGroup.metrics, function (agg) { - return agg.type && (agg.type.name === 'count' || agg.type.name === 'sum'); - }); - } - - if (scaleMetrics) { + if (all) { output.metricScale = interval.scale; output.metricScaleText = interval.preScaled.description; } } - }, - - { - name: 'customInterval', - default: '2h', - write: _.noop - }, + } + }, - { - name: 'format' - }, + { + name: 'customInterval', + default: '2h', + write: _.noop + }, - { - name: 'min_doc_count', - default: 1 - }, + { + name: 'format' + }, - { - name: 'extended_bounds', - default: {}, - write: function (agg, output) { - var val = agg.params.extended_bounds; + { + name: 'min_doc_count', + default: 1 + }, - if (val.min != null || val.max != null) { - output.params.extended_bounds = { - min: moment(val.min).valueOf(), - max: moment(val.max).valueOf() - }; + { + name: 'extended_bounds', + default: {}, + write: function (agg, output) { + const val = agg.params.extended_bounds; - return; - } + if (val.min != null || val.max != null) { + output.params.extended_bounds = { + min: moment(val.min).valueOf(), + max: moment(val.max).valueOf() + }; - var bounds = timefilter.getActiveBounds(); - if (bounds) { - output.params.extended_bounds = { - min: moment(bounds.min).valueOf(), - max: moment(bounds.max).valueOf() - }; - } + return; } } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/date_range.js b/src/ui/public/agg_types/buckets/date_range.js index 9296485a425b2..1101cc72b4398 100644 --- a/src/ui/public/agg_types/buckets/date_range.js +++ b/src/ui/public/agg_types/buckets/date_range.js @@ -1,42 +1,44 @@ -define(function (require) { - var moment = require('moment'); - var dateRange = require('ui/utils/date_range'); - require('ui/directives/validate_date_math'); +import moment from 'moment'; +import dateRange from 'ui/utils/date_range'; +import 'ui/directives/validate_date_math'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterDateRangeProvider from 'ui/agg_types/buckets/create_filter/date_range'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import dateRangesTemplate from 'ui/agg_types/controls/date_ranges.html'; - return function DateRangeAggDefinition(Private, config) { - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/date_range')); - var fieldFormats = Private(require('ui/registry/field_formats')); +export default function DateRangeAggDefinition(Private, config) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterDateRangeProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); - return new BucketAggType({ - name: 'date_range', - title: 'Date Range', - createFilter: createFilter, - getKey: function (bucket, key, agg) { - var formatter = agg.fieldOwnFormatter('text', fieldFormats.getDefaultInstance('date')); - return dateRange.toString(bucket, formatter); - }, - getFormat: function () { - return fieldFormats.getDefaultInstance('string'); - }, - makeLabel: function (aggConfig) { - return aggConfig.params.field.displayName + ' date ranges'; - }, - params: [{ - name: 'field', - filterFieldTypes: 'date', - default: function (agg) { - return agg.vis.indexPattern.timeFieldName; - } - }, { - name: 'ranges', - default: [{ - from: 'now-1w/w', - to: 'now' - }], - editor: require('ui/agg_types/controls/date_ranges.html') - }] - }); - }; -}); + return new BucketAggType({ + name: 'date_range', + title: 'Date Range', + createFilter: createFilter, + getKey: function (bucket, key, agg) { + let formatter = agg.fieldOwnFormatter('text', fieldFormats.getDefaultInstance('date')); + return dateRange.toString(bucket, formatter); + }, + getFormat: function () { + return fieldFormats.getDefaultInstance('string'); + }, + makeLabel: function (aggConfig) { + return aggConfig.params.field.displayName + ' date ranges'; + }, + params: [{ + name: 'field', + filterFieldTypes: 'date', + default: function (agg) { + return agg.vis.indexPattern.timeFieldName; + } + }, { + name: 'ranges', + default: [{ + from: 'now-1w/w', + to: 'now' + }], + editor: dateRangesTemplate + }] + }); +}; diff --git a/src/ui/public/agg_types/buckets/filters.js b/src/ui/public/agg_types/buckets/filters.js index 4acee45b705f7..47ab9c13bd0c7 100644 --- a/src/ui/public/agg_types/buckets/filters.js +++ b/src/ui/public/agg_types/buckets/filters.js @@ -1,45 +1,48 @@ -define(function (require) { - return function FiltersAggDefinition(Private, Notifier) { - var _ = require('lodash'); - var angular = require('angular'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/filters')); - var decorateQuery = Private(require('ui/courier/data_source/_decorate_query')); - var notif = new Notifier({ location: 'Filters Agg' }); +import _ from 'lodash'; +import angular from 'angular'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterFiltersProvider from 'ui/agg_types/buckets/create_filter/filters'; +import DecorateQueryProvider from 'ui/courier/data_source/_decorate_query'; +import filtersTemplate from 'ui/agg_types/controls/filters.html'; +export default function FiltersAggDefinition(Private, Notifier) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterFiltersProvider); + let decorateQuery = Private(DecorateQueryProvider); + let notif = new Notifier({ location: 'Filters Agg' }); - return new BucketAggType({ - name: 'filters', - title: 'Filters', - createFilter: createFilter, - params: [ - { - name: 'filters', - editor: require('ui/agg_types/controls/filters.html'), - default: [ {input: {}, label: ''} ], - write: function (aggConfig, output) { - var inFilters = aggConfig.params.filters; - if (!_.size(inFilters)) return; + return new BucketAggType({ + name: 'filters', + title: 'Filters', + createFilter: createFilter, + customLabels: false, + params: [ + { + name: 'filters', + editor: filtersTemplate, + default: [ {input: {}, label: ''} ], + write: function (aggConfig, output) { + let inFilters = aggConfig.params.filters; + if (!_.size(inFilters)) return; - var outFilters = _.transform(inFilters, function (filters, filter) { - var input = filter.input; - if (!input) return notif.log('malformed filter agg params, missing "input" query'); + let outFilters = _.transform(inFilters, function (filters, filter) { + let input = filter.input; + if (!input) return notif.log('malformed filter agg params, missing "input" query'); - var query = input.query; - if (!query) return notif.log('malformed filter agg params, missing "query" on input'); + let query = input.query; + if (!query) return notif.log('malformed filter agg params, missing "query" on input'); - decorateQuery(query); + decorateQuery(query); - var label = filter.label || _.get(query, 'query_string.query') || angular.toJson(query); - filters[label] = input; - }, {}); + let label = filter.label || _.get(query, 'query_string.query') || angular.toJson(query); + filters[label] = input; + }, {}); - if (!_.size(outFilters)) return; + if (!_.size(outFilters)) return; - var params = output.params || (output.params = {}); - params.filters = outFilters; - } + let params = output.params || (output.params = {}); + params.filters = outFilters; } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/geo_hash.js b/src/ui/public/agg_types/buckets/geo_hash.js index 471b051fc7df6..ebc96eae78c42 100644 --- a/src/ui/public/agg_types/buckets/geo_hash.js +++ b/src/ui/public/agg_types/buckets/geo_hash.js @@ -1,65 +1,85 @@ -define(function (require) { - return function GeoHashAggDefinition(Private, config) { - var _ = require('lodash'); - var moment = require('moment'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var defaultPrecision = 2; +import _ from 'lodash'; +import moment from 'moment'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import precisionTemplate from 'ui/agg_types/controls/precision.html'; +export default function GeoHashAggDefinition(Private, config) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let defaultPrecision = 2; - function getPrecision(precision) { - var maxPrecision = _.parseInt(config.get('visualization:tileMap:maxPrecision')); + // zoomPrecision maps event.zoom to a geohash precision value + // event.limit is the configurable max geohash precision + // default max precision is 7, configurable up to 12 + const zoomPrecision = { + 1: 2, + 2: 2, + 3: 2, + 4: 3, + 5: 3, + 6: 4, + 7: 4, + 8: 5, + 9: 5, + 10: 6, + 11: 6, + 12: 7, + 13: 7, + 14: 8, + 15: 9, + 16: 10, + 17: 11, + 18: 12 + }; - precision = parseInt(precision, 10); + function getPrecision(precision) { + let maxPrecision = _.parseInt(config.get('visualization:tileMap:maxPrecision')); - if (isNaN(precision)) { - precision = defaultPrecision; - } + precision = parseInt(precision, 10); - if (precision > maxPrecision) { - return maxPrecision; - } + if (isNaN(precision)) { + precision = defaultPrecision; + } - return precision; + if (precision > maxPrecision) { + return maxPrecision; } - return new BucketAggType({ - name: 'geohash_grid', - title: 'Geohash', - params: [ - { - name: 'field', - filterFieldTypes: 'geo_point' - }, - { - name: 'autoPrecision', - default: true, - write: _.noop - }, - { - name: 'mapZoom', - write: _.noop - }, - { - name: 'mapCenter', - write: _.noop + return precision; + } + + return new BucketAggType({ + name: 'geohash_grid', + title: 'Geohash', + params: [ + { + name: 'field', + filterFieldTypes: 'geo_point' + }, + { + name: 'autoPrecision', + default: true, + write: _.noop + }, + { + name: 'mapZoom', + write: _.noop + }, + { + name: 'mapCenter', + write: _.noop + }, + { + name: 'precision', + editor: precisionTemplate, + deserialize: getPrecision, + controller: function ($scope) { }, - { - name: 'precision', - default: defaultPrecision, - editor: require('ui/agg_types/controls/precision.html'), - controller: function ($scope) { - $scope.$watchMulti([ - 'agg.params.autoPrecision', - 'outputAgg.params.precision' - ], function (cur, prev) { - if (cur[1]) $scope.agg.params.precision = cur[1]; - }); - }, - deserialize: getPrecision, - write: function (aggConfig, output) { - output.params.precision = getPrecision(aggConfig.params.precision); - } + write: function (aggConfig, output) { + const vis = aggConfig.vis; + const currZoom = vis.hasUiState() && vis.uiStateVal('mapZoom'); + const autoPrecisionVal = zoomPrecision[(currZoom || vis.params.mapZoom)]; + output.params.precision = aggConfig.params.autoPrecision ? autoPrecisionVal : getPrecision(aggConfig.params.precision); } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/histogram.js b/src/ui/public/agg_types/buckets/histogram.js index 416b387d01620..bbfd761b15228 100644 --- a/src/ui/public/agg_types/buckets/histogram.js +++ b/src/ui/public/agg_types/buckets/histogram.js @@ -1,72 +1,75 @@ -define(function (require) { - return function HistogramAggDefinition(Private) { - var _ = require('lodash'); - var moment = require('moment'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/histogram')); +import _ from 'lodash'; +import moment from 'moment'; +import 'ui/validate_date_interval'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterHistogramProvider from 'ui/agg_types/buckets/create_filter/histogram'; +import intervalTemplate from 'ui/agg_types/controls/interval.html'; +import minDocCountTemplate from 'ui/agg_types/controls/min_doc_count.html'; +import extendedBoundsTemplate from 'ui/agg_types/controls/extended_bounds.html'; +export default function HistogramAggDefinition(Private) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterHistogramProvider); - require('ui/validateDateInterval'); - return new BucketAggType({ - name: 'histogram', - title: 'Histogram', - ordered: {}, - makeLabel: function (aggConfig) { - return aggConfig.params.field.displayName; + return new BucketAggType({ + name: 'histogram', + title: 'Histogram', + ordered: {}, + makeLabel: function (aggConfig) { + return aggConfig.params.field.displayName; + }, + createFilter: createFilter, + params: [ + { + name: 'field', + filterFieldTypes: 'number' }, - createFilter: createFilter, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - }, - { - name: 'interval', - editor: require('ui/agg_types/controls/interval.html'), - write: function (aggConfig, output) { - output.params.interval = parseInt(aggConfig.params.interval, 10); - } - }, + { + name: 'interval', + editor: intervalTemplate, + write: function (aggConfig, output) { + output.params.interval = parseInt(aggConfig.params.interval, 10); + } + }, - { - name: 'min_doc_count', - default: null, - editor: require('ui/agg_types/controls/min_doc_count.html'), - write: function (aggConfig, output) { - if (aggConfig.params.min_doc_count) { - output.params.min_doc_count = 0; - } + { + name: 'min_doc_count', + default: null, + editor: minDocCountTemplate, + write: function (aggConfig, output) { + if (aggConfig.params.min_doc_count) { + output.params.min_doc_count = 0; } - }, + } + }, - { - name: 'extended_bounds', - default: {}, - editor: require('ui/agg_types/controls/extended_bounds.html'), - write: function (aggConfig, output) { - var val = aggConfig.params.extended_bounds; + { + name: 'extended_bounds', + default: {}, + editor: extendedBoundsTemplate, + write: function (aggConfig, output) { + let val = aggConfig.params.extended_bounds; - if (val.min != null || val.max != null) { - output.params.extended_bounds = { - min: val.min, - max: val.max - }; - } - }, + if (aggConfig.params.min_doc_count && (val.min != null || val.max != null)) { + output.params.extended_bounds = { + min: val.min, + max: val.max + }; + } + }, - // called from the editor - shouldShow: function (aggConfig) { - var field = aggConfig.params.field; - if ( - field - && (field.type === 'number' || field.type === 'date') - ) { - return aggConfig.params.min_doc_count; - } + // called from the editor + shouldShow: function (aggConfig) { + let field = aggConfig.params.field; + if ( + field + && (field.type === 'number' || field.type === 'date') + ) { + return aggConfig.params.min_doc_count; } } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/ip_range.js b/src/ui/public/agg_types/buckets/ip_range.js index 2eeee56bd0261..ea1a446101625 100644 --- a/src/ui/public/agg_types/buckets/ip_range.js +++ b/src/ui/public/agg_types/buckets/ip_range.js @@ -1,46 +1,47 @@ -define(function (require) { - var _ = require('lodash'); - require('ui/directives/validate_ip'); - require('ui/directives/validate_cidr_mask'); +import _ from 'lodash'; +import 'ui/directives/validate_ip'; +import 'ui/directives/validate_cidr_mask'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterIpRangeProvider from 'ui/agg_types/buckets/create_filter/ip_range'; +import ipRangesTemplate from 'ui/agg_types/controls/ip_ranges.html'; - return function RangeAggDefinition(Private) { - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/ip_range')); +export default function RangeAggDefinition(Private) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterIpRangeProvider); - return new BucketAggType({ - name: 'ip_range', - title: 'IPv4 Range', - createFilter: createFilter, - makeLabel: function (aggConfig) { - return aggConfig.params.field.displayName + ' IP ranges'; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'ip' - }, { - name: 'ipRangeType', - default: 'fromTo', - write: _.noop - }, { - name: 'ranges', - default: { - fromTo: [ - {from: '0.0.0.0', to: '127.255.255.255'}, - {from: '128.0.0.0', to: '191.255.255.255'} - ], - mask: [ - {mask: '0.0.0.0/1'}, - {mask: '128.0.0.0/2'} - ] - }, - editor: require('ui/agg_types/controls/ip_ranges.html'), - write: function (aggConfig, output) { - var ipRangeType = aggConfig.params.ipRangeType; - output.params.ranges = aggConfig.params.ranges[ipRangeType]; - } + return new BucketAggType({ + name: 'ip_range', + title: 'IPv4 Range', + createFilter: createFilter, + makeLabel: function (aggConfig) { + return aggConfig.params.field.displayName + ' IP ranges'; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'ip' + }, { + name: 'ipRangeType', + default: 'fromTo', + write: _.noop + }, { + name: 'ranges', + default: { + fromTo: [ + {from: '0.0.0.0', to: '127.255.255.255'}, + {from: '128.0.0.0', to: '191.255.255.255'} + ], + mask: [ + {mask: '0.0.0.0/1'}, + {mask: '128.0.0.0/2'} + ] + }, + editor: ipRangesTemplate, + write: function (aggConfig, output) { + let ipRangeType = aggConfig.params.ipRangeType; + output.params.ranges = aggConfig.params.ranges[ipRangeType]; } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/range.js b/src/ui/public/agg_types/buckets/range.js index 8f5023f111c65..ec6d33935b7c8 100644 --- a/src/ui/public/agg_types/buckets/range.js +++ b/src/ui/public/agg_types/buckets/range.js @@ -1,71 +1,74 @@ -define(function (require) { - return function RangeAggDefinition(Private) { - var _ = require('lodash'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/range')); - var FieldFormat = Private(require('ui/index_patterns/_field_format/FieldFormat')); - var RangeKey = Private(require('./RangeKey')); +import _ from 'lodash'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterRangeProvider from 'ui/agg_types/buckets/create_filter/range'; +import IndexPatternsFieldFormatFieldFormatProvider from 'ui/index_patterns/_field_format/field_format'; +import RangeKeyProvider from './range_key'; +import rangesTemplate from 'ui/agg_types/controls/ranges.html'; +export default function RangeAggDefinition(Private) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterRangeProvider); + let FieldFormat = Private(IndexPatternsFieldFormatFieldFormatProvider); + let RangeKey = Private(RangeKeyProvider); - var keyCaches = new WeakMap(); - var formats = new WeakMap(); + let keyCaches = new WeakMap(); + let formats = new WeakMap(); - return new BucketAggType({ - name: 'range', - title: 'Range', - createFilter: createFilter, - makeLabel: function (aggConfig) { - return aggConfig.params.field.displayName + ' ranges'; - }, - getKey: function (bucket, key, agg) { - var keys = keyCaches.get(agg); + return new BucketAggType({ + name: 'range', + title: 'Range', + createFilter: createFilter, + makeLabel: function (aggConfig) { + return aggConfig.params.field.displayName + ' ranges'; + }, + getKey: function (bucket, key, agg) { + let keys = keyCaches.get(agg); - if (!keys) { - keys = new Map(); - keyCaches.set(agg, keys); - } + if (!keys) { + keys = new Map(); + keyCaches.set(agg, keys); + } - var id = RangeKey.idBucket(bucket); + let id = RangeKey.idBucket(bucket); - var key = keys.get(id); - if (!key) { - key = new RangeKey(bucket); - keys.set(id, key); - } + key = keys.get(id); + if (!key) { + key = new RangeKey(bucket); + keys.set(id, key); + } - return key; - }, - getFormat: function (agg) { - let format = formats.get(agg); - if (format) return format; + return key; + }, + getFormat: function (agg) { + let format = formats.get(agg); + if (format) return format; - let RangeFormat = FieldFormat.from(function (range) { - let format = agg.fieldOwnFormatter(); - return `${format(range.gte)} to ${format(range.lt)}`; - }); + let RangeFormat = FieldFormat.from(function (range) { + let format = agg.fieldOwnFormatter(); + return `${format(range.gte)} to ${format(range.lt)}`; + }); - format = new RangeFormat(); + format = new RangeFormat(); - formats.set(agg, format); - return format; + formats.set(agg, format); + return format; + }, + params: [ + { + name: 'field', + filterFieldTypes: ['number'] }, - params: [ - { - name: 'field', - filterFieldTypes: ['number'] - }, - { - name: 'ranges', - default: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 } - ], - editor: require('ui/agg_types/controls/ranges.html'), - write: function (aggConfig, output) { - output.params.ranges = aggConfig.params.ranges; - output.params.keyed = true; - } + { + name: 'ranges', + default: [ + { from: 0, to: 1000 }, + { from: 1000, to: 2000 } + ], + editor: rangesTemplate, + write: function (aggConfig, output) { + output.params.ranges = aggConfig.params.ranges; + output.params.keyed = true; } - ] - }); - }; -}); + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/range_key.js b/src/ui/public/agg_types/buckets/range_key.js new file mode 100644 index 0000000000000..27508492855d0 --- /dev/null +++ b/src/ui/public/agg_types/buckets/range_key.js @@ -0,0 +1,25 @@ +export default function () { + + const id = Symbol('id'); + + class RangeKey { + constructor(bucket) { + this.gte = bucket.from == null ? -Infinity : bucket.from; + this.lt = bucket.to == null ? +Infinity : bucket.to; + + this[id] = RangeKey.idBucket(bucket); + } + + + static idBucket(bucket) { + return `from:${bucket.from},to:${bucket.to}`; + } + + toString() { + return this[id]; + } + } + + + return RangeKey; +}; diff --git a/src/ui/public/agg_types/buckets/significant_terms.js b/src/ui/public/agg_types/buckets/significant_terms.js index 65b833ea124c9..36fe87d970d8c 100644 --- a/src/ui/public/agg_types/buckets/significant_terms.js +++ b/src/ui/public/agg_types/buckets/significant_terms.js @@ -1,36 +1,37 @@ -define(function (require) { - return function SignificantTermsAggDefinition(Private) { - var _ = require('lodash'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/terms')); +import _ from 'lodash'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsCreateFilterTermsProvider from 'ui/agg_types/buckets/create_filter/terms'; +import orderAndSizeTemplate from 'ui/agg_types/controls/order_and_size.html'; +export default function SignificantTermsAggDefinition(Private) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); - return new BucketAggType({ - name: 'significant_terms', - title: 'Significant Terms', - makeLabel: function (aggConfig) { - return 'Top ' + aggConfig.params.size + ' unusual terms in ' + aggConfig.params.field.displayName; + return new BucketAggType({ + name: 'significant_terms', + title: 'Significant Terms', + makeLabel: function (aggConfig) { + return 'Top ' + aggConfig.params.size + ' unusual terms in ' + aggConfig.params.field.displayName; + }, + createFilter: createFilter, + params: [ + { + name: 'field', + filterFieldTypes: 'string' }, - createFilter: createFilter, - params: [ - { - name: 'field', - filterFieldTypes: 'string' - }, - { - name: 'size', - editor: require('ui/agg_types/controls/order_and_size.html'), - }, - { - name: 'exclude', - type: 'regex', - advanced: true - }, - { - name: 'include', - type: 'regex', - advanced: true - } - ] - }); - }; -}); + { + name: 'size', + editor: orderAndSizeTemplate, + }, + { + name: 'exclude', + type: 'regex', + advanced: true + }, + { + name: 'include', + type: 'regex', + advanced: true + } + ] + }); +}; diff --git a/src/ui/public/agg_types/buckets/terms.js b/src/ui/public/agg_types/buckets/terms.js index c9eecec142e1b..4a81cbbf939ff 100644 --- a/src/ui/public/agg_types/buckets/terms.js +++ b/src/ui/public/agg_types/buckets/terms.js @@ -1,177 +1,182 @@ -define(function (require) { - return function TermsAggDefinition(Private) { - var _ = require('lodash'); - var BucketAggType = Private(require('ui/agg_types/buckets/_bucket_agg_type')); - var bucketCountBetween = Private(require('ui/agg_types/buckets/_bucket_count_between')); - var AggConfig = Private(require('ui/Vis/AggConfig')); - var Schemas = Private(require('ui/Vis/Schemas')); - var createFilter = Private(require('ui/agg_types/buckets/create_filter/terms')); - - var orderAggSchema = (new Schemas([ - { - group: 'none', - name: 'orderAgg', - title: 'Order Agg', - aggFilter: ['!percentiles', '!median', '!std_dev'] - } - ])).all[0]; - - function isNotType(type) { - return function (agg) { - var field = agg.params.field; - return !field || field.type !== type; - }; +import _ from 'lodash'; +import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_agg_type'; +import AggTypesBucketsBucketCountBetweenProvider from 'ui/agg_types/buckets/_bucket_count_between'; +import VisAggConfigProvider from 'ui/vis/agg_config'; +import VisSchemasProvider from 'ui/vis/schemas'; +import AggTypesBucketsCreateFilterTermsProvider from 'ui/agg_types/buckets/create_filter/terms'; +import orderAggTemplate from 'ui/agg_types/controls/order_agg.html'; +import orderAndSizeTemplate from 'ui/agg_types/controls/order_and_size.html'; +export default function TermsAggDefinition(Private) { + let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider); + let bucketCountBetween = Private(AggTypesBucketsBucketCountBetweenProvider); + let AggConfig = Private(VisAggConfigProvider); + let Schemas = Private(VisSchemasProvider); + let createFilter = Private(AggTypesBucketsCreateFilterTermsProvider); + + let orderAggSchema = (new Schemas([ + { + group: 'none', + name: 'orderAgg', + title: 'Order Agg', + aggFilter: ['!percentiles', '!median', '!std_dev'] } - - return new BucketAggType({ - name: 'terms', - title: 'Terms', - makeLabel: function (agg) { - var params = agg.params; - return params.field.displayName + ': ' + params.order.display; + ])).all[0]; + + function isNotType(type) { + return function (agg) { + let field = agg.params.field; + return !field || field.type !== type; + }; + } + + return new BucketAggType({ + name: 'terms', + title: 'Terms', + makeLabel: function (agg) { + let params = agg.params; + return params.field.displayName + ': ' + params.order.display; + }, + createFilter: createFilter, + params: [ + { + name: 'field', + scriptable: true, + filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] }, - createFilter: createFilter, - params: [ - { - name: 'field', - scriptable: true, - filterFieldTypes: ['number', 'boolean', 'date', 'ip', 'string'] - }, - { - name: 'exclude', - type: 'regex', - advanced: true, - disabled: isNotType('string') + { + name: 'exclude', + type: 'regex', + advanced: true, + disabled: isNotType('string') + }, + { + name: 'include', + type: 'regex', + advanced: true, + disabled: isNotType('string') + }, + { + name: 'size', + default: 5 + }, + { + name: 'orderAgg', + type: AggConfig, + default: null, + editor: orderAggTemplate, + serialize: function (orderAgg) { + return orderAgg.toJSON(); }, - { - name: 'include', - type: 'regex', - advanced: true, - disabled: isNotType('string') + deserialize: function (state, agg) { + return this.makeOrderAgg(agg, state); }, - { - name: 'size', - default: 5 + makeOrderAgg: function (termsAgg, state) { + state = state || {}; + state.schema = orderAggSchema; + let orderAgg = new AggConfig(termsAgg.vis, state); + orderAgg.id = termsAgg.id + '-orderAgg'; + return orderAgg; }, - { - name: 'orderAgg', - type: AggConfig, - default: null, - editor: require('ui/agg_types/controls/order_agg.html'), - serialize: function (orderAgg) { - return orderAgg.toJSON(); - }, - deserialize: function (state, agg) { - return this.makeOrderAgg(agg, state); - }, - makeOrderAgg: function (termsAgg, state) { - state = state || {}; - state.schema = orderAggSchema; - var orderAgg = new AggConfig(termsAgg.vis, state); - orderAgg.id = termsAgg.id + '-orderAgg'; - return orderAgg; - }, - controller: function ($scope) { - $scope.safeMakeLabel = function (agg) { - try { - return agg.makeLabel(); - } catch (e) { - return '- agg not valid -'; - } - }; - - var INIT = {}; // flag to know when prevOrderBy has changed - var prevOrderBy = INIT; - - $scope.$watch('responseValueAggs', updateOrderAgg); - $scope.$watch('agg.params.orderBy', updateOrderAgg); - - function updateOrderAgg() { - var agg = $scope.agg; - var aggs = agg.vis.aggs; - var params = agg.params; - var orderBy = params.orderBy; - var paramDef = agg.type.params.byName.orderAgg; - - // setup the initial value of orderBy - if (!orderBy && prevOrderBy === INIT) { - // abort until we get the responseValueAggs - if (!$scope.responseValueAggs) return; - params.orderBy = (_.first($scope.responseValueAggs) || { id: 'custom' }).id; - return; - } - - // track the previous value - prevOrderBy = orderBy; + controller: function ($scope) { + $scope.safeMakeLabel = function (agg) { + try { + return agg.makeLabel(); + } catch (e) { + return '- agg not valid -'; + } + }; + + let INIT = {}; // flag to know when prevOrderBy has changed + let prevOrderBy = INIT; + + $scope.$watch('responseValueAggs', updateOrderAgg); + $scope.$watch('agg.params.orderBy', updateOrderAgg); + + function updateOrderAgg() { + let agg = $scope.agg; + let aggs = agg.vis.aggs; + let params = agg.params; + let orderBy = params.orderBy; + let paramDef = agg.type.params.byName.orderAgg; + + // setup the initial value of orderBy + if (!orderBy && prevOrderBy === INIT) { + // abort until we get the responseValueAggs + if (!$scope.responseValueAggs) return; + params.orderBy = (_.first($scope.responseValueAggs) || { id: 'custom' }).id; + return; + } - // we aren't creating a custom aggConfig - if (!orderBy || orderBy !== 'custom') { - params.orderAgg = null; + // track the previous value + prevOrderBy = orderBy; - if (orderBy === '_term') { - params.orderBy = '_term'; - return; - } + // we aren't creating a custom aggConfig + if (!orderBy || orderBy !== 'custom') { + params.orderAgg = null; - // ensure that orderBy is set to a valid agg - if (!_.find($scope.responseValueAggs, { id: orderBy })) { - params.orderBy = null; - } + if (orderBy === '_term') { + params.orderBy = '_term'; return; } - params.orderAgg = params.orderAgg || paramDef.makeOrderAgg(agg); - } - }, - write: function (agg, output) { - var vis = agg.vis; - var dir = agg.params.order.val; - var order = output.params.order = {}; - - var orderAgg = agg.params.orderAgg || vis.aggs.getResponseAggById(agg.params.orderBy); - - // TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings - // thus causing issues with filtering. This probably causes other issues since float might not - // be able to contain the number on the elasticsearch side - if (output.params.script) { - output.params.valueType = agg.field().type === 'number' ? 'float' : agg.field().type; - } - - if (!orderAgg) { - order[agg.params.orderBy || '_count'] = dir; + // ensure that orderBy is set to a valid agg + if (!_.find($scope.responseValueAggs, { id: orderBy })) { + params.orderBy = null; + } return; } - if (orderAgg.type.name === 'count') { - order._count = dir; - return; - } + params.orderAgg = params.orderAgg || paramDef.makeOrderAgg(agg); + } + }, + write: function (agg, output) { + let vis = agg.vis; + let dir = agg.params.order.val; + let order = output.params.order = {}; + + let orderAgg = agg.params.orderAgg || vis.aggs.getResponseAggById(agg.params.orderBy); + + // TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings + // thus causing issues with filtering. This probably causes other issues since float might not + // be able to contain the number on the elasticsearch side + if (output.params.script) { + output.params.valueType = agg.field().type === 'number' ? 'float' : agg.field().type; + } - var orderAggId = orderAgg.id; - if (orderAgg.parentId) { - orderAgg = vis.aggs.byId[orderAgg.parentId]; - } + if (!orderAgg) { + order[agg.params.orderBy || '_count'] = dir; + return; + } - output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAggId] = dir; + if (orderAgg.type.name === 'count') { + order._count = dir; + return; } - }, - { - name: 'order', - type: 'optioned', - default: 'desc', - editor: require('ui/agg_types/controls/order_and_size.html'), - options: [ - { display: 'Descending', val: 'desc' }, - { display: 'Ascending', val: 'asc' } - ], - write: _.noop // prevent default write, it's handled by orderAgg - }, - { - name: 'orderBy', - write: _.noop // prevent default write, it's handled by orderAgg + + let orderAggId = orderAgg.id; + if (orderAgg.parentId) { + orderAgg = vis.aggs.byId[orderAgg.parentId]; + } + + output.subAggs = (output.subAggs || []).concat(orderAgg); + order[orderAggId] = dir; } - ] - }); - }; -}); + }, + { + name: 'order', + type: 'optioned', + default: 'desc', + editor: orderAndSizeTemplate, + options: [ + { display: 'Descending', val: 'desc' }, + { display: 'Ascending', val: 'asc' } + ], + write: _.noop // prevent default write, it's handled by orderAgg + }, + { + name: 'orderBy', + write: _.noop // prevent default write, it's handled by orderAgg + } + ] + }); +}; diff --git a/src/ui/public/agg_types/controls/field.html b/src/ui/public/agg_types/controls/field.html index 765c82533db38..8232015e6867f 100644 --- a/src/ui/public/agg_types/controls/field.html +++ b/src/ui/public/agg_types/controls/field.html @@ -9,7 +9,7 @@

    - Careful! The field selected contains analyzed strings. Analyzed strings are highly unique and can use a lot of memory to visualize. Values such as foo-bar will be broken into foo and bar. See Mapping Core Types for more information on setting this field as not_analyzed + Careful! The field selected contains analyzed strings. Analyzed strings are highly unique and can use a lot of memory to visualize. Values such as foo-bar will be broken into foo and bar. See Mapping Types for more information on setting this field as not_analyzed

    diff --git a/src/ui/public/agg_types/controls/regular_expression.html b/src/ui/public/agg_types/controls/regular_expression.html index e9b405752ba42..2857ffa4b805e 100644 --- a/src/ui/public/agg_types/controls/regular_expression.html +++ b/src/ui/public/agg_types/controls/regular_expression.html @@ -7,20 +7,4 @@ ng-model="agg.params[aggParam.name].pattern" >

    -
    -
    - - - - -
    -
    -
    \ No newline at end of file +
    diff --git a/src/ui/public/agg_types/index.js b/src/ui/public/agg_types/index.js index 16395c10f8aab..762a9dbab4a99 100644 --- a/src/ui/public/agg_types/index.js +++ b/src/ui/public/agg_types/index.js @@ -1,64 +1,81 @@ -define(function (require) { - return function AggTypeService(Private) { - var IndexedArray = require('ui/IndexedArray'); +import IndexedArray from 'ui/indexed_array'; +import 'ui/agg_types/agg_params'; +import AggTypesMetricsCountProvider from 'ui/agg_types/metrics/count'; +import AggTypesMetricsAvgProvider from 'ui/agg_types/metrics/avg'; +import AggTypesMetricsSumProvider from 'ui/agg_types/metrics/sum'; +import AggTypesMetricsMedianProvider from 'ui/agg_types/metrics/median'; +import AggTypesMetricsMinProvider from 'ui/agg_types/metrics/min'; +import AggTypesMetricsMaxProvider from 'ui/agg_types/metrics/max'; +import AggTypesMetricsStdDeviationProvider from 'ui/agg_types/metrics/std_deviation'; +import AggTypesMetricsCardinalityProvider from 'ui/agg_types/metrics/cardinality'; +import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles'; +import AggTypesMetricsPercentileRanksProvider from 'ui/agg_types/metrics/percentile_ranks'; +import AggTypesBucketsDateHistogramProvider from 'ui/agg_types/buckets/date_histogram'; +import AggTypesBucketsHistogramProvider from 'ui/agg_types/buckets/histogram'; +import AggTypesBucketsRangeProvider from 'ui/agg_types/buckets/range'; +import AggTypesBucketsDateRangeProvider from 'ui/agg_types/buckets/date_range'; +import AggTypesBucketsIpRangeProvider from 'ui/agg_types/buckets/ip_range'; +import AggTypesBucketsTermsProvider from 'ui/agg_types/buckets/terms'; +import AggTypesBucketsFiltersProvider from 'ui/agg_types/buckets/filters'; +import AggTypesBucketsSignificantTermsProvider from 'ui/agg_types/buckets/significant_terms'; +import AggTypesBucketsGeoHashProvider from 'ui/agg_types/buckets/geo_hash'; +export default function AggTypeService(Private) { - var aggs = { - metrics: [ - Private(require('ui/agg_types/metrics/count')), - Private(require('ui/agg_types/metrics/avg')), - Private(require('ui/agg_types/metrics/sum')), - Private(require('ui/agg_types/metrics/median')), - Private(require('ui/agg_types/metrics/min')), - Private(require('ui/agg_types/metrics/max')), - Private(require('ui/agg_types/metrics/stdDeviation')), - Private(require('ui/agg_types/metrics/cardinality')), - Private(require('ui/agg_types/metrics/percentiles')), - Private(require('ui/agg_types/metrics/percentile_ranks')) - ], - buckets: [ - Private(require('ui/agg_types/buckets/date_histogram')), - Private(require('ui/agg_types/buckets/histogram')), - Private(require('ui/agg_types/buckets/range')), - Private(require('ui/agg_types/buckets/date_range')), - Private(require('ui/agg_types/buckets/ip_range')), - Private(require('ui/agg_types/buckets/terms')), - Private(require('ui/agg_types/buckets/filters')), - Private(require('ui/agg_types/buckets/significant_terms')), - Private(require('ui/agg_types/buckets/geo_hash')) - ] - }; + let aggs = { + metrics: [ + Private(AggTypesMetricsCountProvider), + Private(AggTypesMetricsAvgProvider), + Private(AggTypesMetricsSumProvider), + Private(AggTypesMetricsMedianProvider), + Private(AggTypesMetricsMinProvider), + Private(AggTypesMetricsMaxProvider), + Private(AggTypesMetricsStdDeviationProvider), + Private(AggTypesMetricsCardinalityProvider), + Private(AggTypesMetricsPercentilesProvider), + Private(AggTypesMetricsPercentileRanksProvider) + ], + buckets: [ + Private(AggTypesBucketsDateHistogramProvider), + Private(AggTypesBucketsHistogramProvider), + Private(AggTypesBucketsRangeProvider), + Private(AggTypesBucketsDateRangeProvider), + Private(AggTypesBucketsIpRangeProvider), + Private(AggTypesBucketsTermsProvider), + Private(AggTypesBucketsFiltersProvider), + Private(AggTypesBucketsSignificantTermsProvider), + Private(AggTypesBucketsGeoHashProvider) + ] + }; - Object.keys(aggs).forEach(function (type) { - aggs[type].forEach(function (agg) { - agg.type = type; - }); + Object.keys(aggs).forEach(function (type) { + aggs[type].forEach(function (agg) { + agg.type = type; }); + }); + + /** + * IndexedArray of Aggregation Types. + * + * These types form two groups, metric and buckets. + * + * @module agg_types + * @type {IndexedArray} + */ + return new IndexedArray({ /** - * IndexedArray of Aggregation Types. - * - * These types form two groups, metric and buckets. - * - * @module agg_types - * @type {IndexedArray} + * @type {Array} */ - return new IndexedArray({ + index: ['name'], - /** - * @type {Array} - */ - index: ['name'], - - /** - * [group description] - * @type {Array} - */ - group: ['type'], - initialSet: aggs.metrics.concat(aggs.buckets) - }); - }; + /** + * [group description] + * @type {Array} + */ + group: ['type'], + initialSet: aggs.metrics.concat(aggs.buckets) + }); +}; - // preload - require('ui/agg_types/AggParams'); -}); +// preload diff --git a/src/ui/public/agg_types/metrics/MetricAggType.js b/src/ui/public/agg_types/metrics/MetricAggType.js deleted file mode 100644 index 85c7c98abdc56..0000000000000 --- a/src/ui/public/agg_types/metrics/MetricAggType.js +++ /dev/null @@ -1,43 +0,0 @@ -define(function (require) { - return function MetricAggTypeProvider(Private) { - var _ = require('lodash'); - var AggType = Private(require('ui/agg_types/AggType')); - var fieldFormats = Private(require('ui/registry/field_formats')); - - _.class(MetricAggType).inherits(AggType); - function MetricAggType(config) { - MetricAggType.Super.call(this, config); - - // allow overriding any value on the prototype - _.forOwn(config, function (val, key) { - if (_.has(MetricAggType.prototype, key)) { - this[key] = val; - } - }, this); - } - - /** - * Read the values for this metric from the - * @param {[type]} bucket [description] - * @return {[type]} [description] - */ - MetricAggType.prototype.getValue = function (agg, bucket) { - 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(); - return field ? field.format : fieldFormats.getDefaultInstance('number'); - }; - - return MetricAggType; - }; -}); diff --git a/src/ui/public/agg_types/metrics/avg.js b/src/ui/public/agg_types/metrics/avg.js index 1a7a1abb755c7..f313464433da1 100644 --- a/src/ui/public/agg_types/metrics/avg.js +++ b/src/ui/public/agg_types/metrics/avg.js @@ -1,19 +1,19 @@ -define(function (require) { - return function AggTypeMetricAvgProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; - return new MetricAggType({ - name: 'avg', - title: 'Average', - makeLabel: function (aggConfig) { - return 'Average ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }); - }; -}); +export default function AggTypeMetricAvgProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + + return new MetricAggType({ + name: 'avg', + title: 'Average', + makeLabel: function (aggConfig) { + return 'Average ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); +}; diff --git a/src/ui/public/agg_types/metrics/cardinality.js b/src/ui/public/agg_types/metrics/cardinality.js index f4eef38de8b42..8ff6d93f61924 100644 --- a/src/ui/public/agg_types/metrics/cardinality.js +++ b/src/ui/public/agg_types/metrics/cardinality.js @@ -1,22 +1,23 @@ -define(function (require) { - return function AggTypeMetricCardinalityProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var fieldFormats = Private(require('ui/registry/field_formats')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; - return new MetricAggType({ - name: 'cardinality', - title: 'Unique Count', - makeLabel: function (aggConfig) { - return 'Unique count of ' + aggConfig.params.field.displayName; - }, - getFormat: function () { - return fieldFormats.getDefaultInstance('number'); - }, - params: [ - { - name: 'field' - } - ] - }); - }; -}); +export default function AggTypeMetricCardinalityProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); + + return new MetricAggType({ + name: 'cardinality', + title: 'Unique Count', + makeLabel: function (aggConfig) { + return 'Unique count of ' + aggConfig.params.field.displayName; + }, + getFormat: function () { + return fieldFormats.getDefaultInstance('number'); + }, + params: [ + { + name: 'field' + } + ] + }); +}; diff --git a/src/ui/public/agg_types/metrics/count.js b/src/ui/public/agg_types/metrics/count.js index a4d49d3084dcb..fcad3d8dffa06 100644 --- a/src/ui/public/agg_types/metrics/count.js +++ b/src/ui/public/agg_types/metrics/count.js @@ -1,21 +1,22 @@ -define(function (require) { - return function AggTypeMetricCountProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var fieldFormats = Private(require('ui/registry/field_formats')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; - return new MetricAggType({ - name: 'count', - title: 'Count', - hasNoDsl: true, - makeLabel: function () { - return 'Count'; - }, - getFormat: function () { - return fieldFormats.getDefaultInstance('number'); - }, - getValue: function (agg, bucket) { - return bucket.doc_count; - } - }); - }; -}); +export default function AggTypeMetricCountProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); + + return new MetricAggType({ + name: 'count', + title: 'Count', + hasNoDsl: true, + makeLabel: function () { + return 'Count'; + }, + getFormat: function () { + return fieldFormats.getDefaultInstance('number'); + }, + getValue: function (agg, bucket) { + return bucket.doc_count; + } + }); +}; diff --git a/src/ui/public/agg_types/metrics/getResponseAggConfigClass.js b/src/ui/public/agg_types/metrics/getResponseAggConfigClass.js deleted file mode 100644 index 69314d64a80b8..0000000000000 --- a/src/ui/public/agg_types/metrics/getResponseAggConfigClass.js +++ /dev/null @@ -1,51 +0,0 @@ -define(function (require) { - return function ResponseAggConfigProvider() { - var _ = require('lodash'); - - /** - * Get the ResponseAggConfig class for an aggConfig, - * which might be cached on the aggConfig or created. - * - * @param {AggConfig} agg - the AggConfig the VAC should inherit from - * @param {object} props - properties that the VAC should have - * @return {Constructor} - a constructor for VAC objects that will inherit the aggConfig - */ - return function getResponseConfigClass(agg, props) { - if (agg.$$_ResponseAggConfigClass) { - return agg.$$_ResponseAggConfigClass; - } else { - return (agg.$$_ResponseAggConfigClass = create(agg, props)); - } - }; - - function create(parentAgg, props) { - - /** - * AggConfig "wrapper" for multi-value metric aggs which - * need to modify AggConfig behavior for each value produced. - * - * @param {string|number} key - the key or index that identifies - * this part of the multi-value - */ - function ResponseAggConfig(key) { - this.key = key; - this.parentId = this.id; - - var subId = String(key); - if (subId.indexOf('.') > -1) { - this.id = this.parentId + '[\'' + subId.replace(/'/g, '\\\'') + '\']'; - } else { - this.id = this.parentId + '.' + subId; - } - } - - ResponseAggConfig.prototype = Object.create(parentAgg, { - constructor: ResponseAggConfig - }); - - _.assign(ResponseAggConfig.prototype, props); - - return ResponseAggConfig; - } - }; -}); diff --git a/src/ui/public/agg_types/metrics/get_response_agg_config_class.js b/src/ui/public/agg_types/metrics/get_response_agg_config_class.js new file mode 100644 index 0000000000000..b9112f343e81b --- /dev/null +++ b/src/ui/public/agg_types/metrics/get_response_agg_config_class.js @@ -0,0 +1,49 @@ +import _ from 'lodash'; +export default function ResponseAggConfigProvider() { + + /** + * Get the ResponseAggConfig class for an aggConfig, + * which might be cached on the aggConfig or created. + * + * @param {AggConfig} agg - the AggConfig the VAC should inherit from + * @param {object} props - properties that the VAC should have + * @return {Constructor} - a constructor for VAC objects that will inherit the aggConfig + */ + return function getResponseConfigClass(agg, props) { + if (agg.$$_ResponseAggConfigClass) { + return agg.$$_ResponseAggConfigClass; + } else { + return (agg.$$_ResponseAggConfigClass = create(agg, props)); + } + }; + + function create(parentAgg, props) { + + /** + * AggConfig "wrapper" for multi-value metric aggs which + * need to modify AggConfig behavior for each value produced. + * + * @param {string|number} key - the key or index that identifies + * this part of the multi-value + */ + function ResponseAggConfig(key) { + this.key = key; + this.parentId = this.id; + + let subId = String(key); + if (subId.indexOf('.') > -1) { + this.id = this.parentId + '[\'' + subId.replace(/'/g, '\\\'') + '\']'; + } else { + this.id = this.parentId + '.' + subId; + } + } + + ResponseAggConfig.prototype = Object.create(parentAgg, { + constructor: ResponseAggConfig + }); + + _.assign(ResponseAggConfig.prototype, props); + + return ResponseAggConfig; + } +}; diff --git a/src/ui/public/agg_types/metrics/max.js b/src/ui/public/agg_types/metrics/max.js index aef10513b421c..938ee8d4a45e7 100644 --- a/src/ui/public/agg_types/metrics/max.js +++ b/src/ui/public/agg_types/metrics/max.js @@ -1,19 +1,19 @@ -define(function (require) { - return function AggTypeMetricMaxProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; - return new MetricAggType({ - name: 'max', - title: 'Max', - makeLabel: function (aggConfig) { - return 'Max ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number,date' - } - ] - }); - }; -}); +export default function AggTypeMetricMaxProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + + return new MetricAggType({ + name: 'max', + title: 'Max', + makeLabel: function (aggConfig) { + return 'Max ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number,date' + } + ] + }); +}; diff --git a/src/ui/public/agg_types/metrics/median.js b/src/ui/public/agg_types/metrics/median.js index 9b8f982ded545..1564aeba278ad 100644 --- a/src/ui/public/agg_types/metrics/median.js +++ b/src/ui/public/agg_types/metrics/median.js @@ -1,29 +1,35 @@ -define(function (require) { - return function AggTypeMetricMaxProvider(Private) { - var _ = require('lodash'); - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var getResponseAggConfigClass = Private(require('ui/agg_types/metrics/getResponseAggConfigClass')); - var percentiles = Private(require('ui/agg_types/metrics/percentiles')); +import _ from 'lodash'; +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import AggTypesMetricsGetResponseAggConfigClassProvider from 'ui/agg_types/metrics/get_response_agg_config_class'; +import AggTypesMetricsPercentilesProvider from 'ui/agg_types/metrics/percentiles'; +export default function AggTypeMetricMedianProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let getResponseAggConfigClass = Private(AggTypesMetricsGetResponseAggConfigClassProvider); + let percentiles = Private(AggTypesMetricsPercentilesProvider); - return new MetricAggType({ - name: 'median', - dslName: 'percentiles', - title: 'Median', - makeLabel: function (aggConfig) { - return 'Median ' + aggConfig.params.field.displayName; + return new MetricAggType({ + name: 'median', + dslName: 'percentiles', + title: 'Median', + makeLabel: function (aggConfig) { + return 'Median ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - }, - { - name: 'percents', - default: [50] + { + name: 'percents', + default: [50] + }, + { + write(agg, output) { + output.params.keyed = false; } - ], - getResponseAggs: percentiles.getResponseAggs, - getValue: percentiles.getValue - }); - }; -}); + } + ], + getResponseAggs: percentiles.getResponseAggs, + getValue: percentiles.getValue + }); +}; diff --git a/src/ui/public/agg_types/metrics/metric_agg_type.js b/src/ui/public/agg_types/metrics/metric_agg_type.js new file mode 100644 index 0000000000000..e9d03f60f3356 --- /dev/null +++ b/src/ui/public/agg_types/metrics/metric_agg_type.js @@ -0,0 +1,50 @@ +import _ from 'lodash'; +import AggTypesAggTypeProvider from 'ui/agg_types/agg_type'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +export default function MetricAggTypeProvider(Private) { + let AggType = Private(AggTypesAggTypeProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); + + _.class(MetricAggType).inherits(AggType); + function MetricAggType(config) { + MetricAggType.Super.call(this, config); + + // allow overriding any value on the prototype + _.forOwn(config, function (val, key) { + if (_.has(MetricAggType.prototype, key)) { + this[key] = val; + } + }, this); + } + + /** + * Read the values for this metric from the + * @param {[type]} bucket [description] + * @return {*} [description] + */ + MetricAggType.prototype.getValue = function (agg, bucket) { + // Metric types where an empty set equals `zero` + let isSettableToZero = ['cardinality', 'sum'].indexOf(agg.__type.name) !== -1; + + // Return proper values when no buckets are present + // `Count` handles empty sets properly + if (!bucket[agg.id] && isSettableToZero) return 0; + + return bucket[agg.id] && 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) { + let field = agg.field(); + return field ? field.format : fieldFormats.getDefaultInstance('number'); + }; + + return MetricAggType; +}; diff --git a/src/ui/public/agg_types/metrics/min.js b/src/ui/public/agg_types/metrics/min.js index 250a5001b1489..a14593af1dd01 100644 --- a/src/ui/public/agg_types/metrics/min.js +++ b/src/ui/public/agg_types/metrics/min.js @@ -1,19 +1,19 @@ -define(function (require) { - return function AggTypeMetricMinProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; - return new MetricAggType({ - name: 'min', - title: 'Min', - makeLabel: function (aggConfig) { - return 'Min ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number,date' - } - ] - }); - }; -}); +export default function AggTypeMetricMinProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + + return new MetricAggType({ + name: 'min', + title: 'Min', + makeLabel: function (aggConfig) { + return 'Min ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number,date' + } + ] + }); +}; diff --git a/src/ui/public/agg_types/metrics/percentile_ranks.js b/src/ui/public/agg_types/metrics/percentile_ranks.js index 4e167c8016776..4400873b2c620 100644 --- a/src/ui/public/agg_types/metrics/percentile_ranks.js +++ b/src/ui/public/agg_types/metrics/percentile_ranks.js @@ -1,58 +1,62 @@ -define(function (require) { - return function AggTypeMetricPercentileRanksProvider(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import valuesEditor from 'ui/agg_types/controls/percentile_ranks.html'; +import 'ui/number_list'; +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import AggTypesMetricsGetResponseAggConfigClassProvider from 'ui/agg_types/metrics/get_response_agg_config_class'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import getPercentileValue from './percentiles_get_value'; - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var getResponseAggConfigClass = Private(require('ui/agg_types/metrics/getResponseAggConfigClass')); - var fieldFormats = Private(require('ui/registry/field_formats')); +export default function AggTypeMetricPercentileRanksProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let getResponseAggConfigClass = Private(AggTypesMetricsGetResponseAggConfigClassProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); - var valuesEditor = require('ui/agg_types/controls/percentile_ranks.html'); - // required by the values editor - require('ui/number_list'); + // required by the values editor - var valueProps = { - makeLabel: function () { - var field = this.field(); - var format = (field && field.format) || fieldFormats.getDefaultInstance('number'); + let valueProps = { + makeLabel: function () { + let field = this.field(); + let format = (field && field.format) || fieldFormats.getDefaultInstance('number'); + const label = this.params.customLabel || this.fieldDisplayName(); - return 'Percentile rank ' + format.convert(this.key, 'text') + ' of "' + this.fieldDisplayName() + '"'; - } - }; - - return new MetricAggType({ - name: 'percentile_ranks', - title: 'Percentile Ranks', - makeLabel: function (agg) { - return 'Percentile ranks of ' + agg.fieldDisplayName(); - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - }, - { - name: 'values', - editor: valuesEditor, - default: [] - } - ], - getResponseAggs: function (agg) { - var ValueAggConfig = getResponseAggConfigClass(agg, valueProps); + return 'Percentile rank ' + format.convert(this.key, 'text') + ' of "' + label + '"'; + } + }; - return agg.params.values.map(function (value) { - return new ValueAggConfig(value); - }); + return new MetricAggType({ + name: 'percentile_ranks', + title: 'Percentile Ranks', + makeLabel: function (agg) { + return 'Percentile ranks of ' + agg.fieldDisplayName(); + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' }, - getFormat: function () { - return fieldFormats.getInstance('percent') || fieldFormats.getDefaultInstance('number'); + { + name: 'values', + editor: valuesEditor, + default: [] }, - 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 - return _.find(bucket[agg.parentId].values, function (value, key) { - return agg.key === parseFloat(key); - }) / 100; + { + write(agg, output) { + output.params.keyed = false; + } } - }); - }; -}); + ], + getResponseAggs: function (agg) { + let ValueAggConfig = getResponseAggConfigClass(agg, valueProps); + + return agg.params.values.map(function (value) { + return new ValueAggConfig(value); + }); + }, + getFormat: function () { + return fieldFormats.getInstance('percent') || fieldFormats.getDefaultInstance('number'); + }, + getValue: function (agg, bucket) { + return getPercentileValue(agg, bucket) / 100; + } + }); +}; diff --git a/src/ui/public/agg_types/metrics/percentiles.js b/src/ui/public/agg_types/metrics/percentiles.js index 101ee08318f61..59ba2da34387f 100644 --- a/src/ui/public/agg_types/metrics/percentiles.js +++ b/src/ui/public/agg_types/metrics/percentiles.js @@ -1,53 +1,55 @@ -define(function (require) { - return function AggTypeMetricPercentilesProvider(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import ordinalSuffix from 'ui/utils/ordinal_suffix'; +import percentsEditor from 'ui/agg_types/controls/percentiles.html'; +import 'ui/number_list'; +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import AggTypesMetricsGetResponseAggConfigClassProvider from 'ui/agg_types/metrics/get_response_agg_config_class'; +import RegistryFieldFormatsProvider from 'ui/registry/field_formats'; +import getPercentileValue from './percentiles_get_value'; - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var getResponseAggConfigClass = Private(require('ui/agg_types/metrics/getResponseAggConfigClass')); - var ordinalSuffix = require('ui/utils/ordinal_suffix'); - var fieldFormats = Private(require('ui/registry/field_formats')); +export default function AggTypeMetricPercentilesProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let getResponseAggConfigClass = Private(AggTypesMetricsGetResponseAggConfigClassProvider); + let fieldFormats = Private(RegistryFieldFormatsProvider); - var percentsEditor = require('ui/agg_types/controls/percentiles.html'); - // required by the percentiles editor - require('ui/number_list'); + // required by the percentiles editor - var valueProps = { - makeLabel: function () { - return ordinalSuffix(this.key) + ' percentile of ' + this.fieldDisplayName(); - } - }; + let valueProps = { + makeLabel: function () { + const label = this.params.customLabel || this.fieldDisplayName(); + return ordinalSuffix(this.key) + ' percentile of ' + label; + } + }; - return new MetricAggType({ - name: 'percentiles', - title: 'Percentiles', - makeLabel: function (agg) { - return 'Percentiles of ' + agg.fieldDisplayName(); + return new MetricAggType({ + name: 'percentiles', + title: 'Percentiles', + makeLabel: function (agg) { + return 'Percentiles of ' + agg.fieldDisplayName(); + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - }, - { - name: 'percents', - editor: percentsEditor, - default: [1, 5, 25, 50, 75, 95, 99] - } - ], - getResponseAggs: function (agg) { - var ValueAggConfig = getResponseAggConfigClass(agg, valueProps); - - return agg.params.percents.map(function (percent) { - return new ValueAggConfig(percent); - }); + { + name: 'percents', + editor: percentsEditor, + default: [1, 5, 25, 50, 75, 95, 99] }, - 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 - return _.find(bucket[agg.parentId].values, function (value, key) { - return agg.key === parseFloat(key); - }); + { + write(agg, output) { + output.params.keyed = false; + } } - }); - }; -}); + ], + getResponseAggs: function (agg) { + let ValueAggConfig = getResponseAggConfigClass(agg, valueProps); + + return agg.params.percents.map(function (percent) { + return new ValueAggConfig(percent); + }); + }, + getValue: getPercentileValue + }); +}; diff --git a/src/ui/public/agg_types/metrics/percentiles_get_value.js b/src/ui/public/agg_types/metrics/percentiles_get_value.js new file mode 100644 index 0000000000000..900f5bfd0d64a --- /dev/null +++ b/src/ui/public/agg_types/metrics/percentiles_get_value.js @@ -0,0 +1,7 @@ +import { find } from 'lodash'; + +export default function getPercentileValue(agg, bucket) { + const values = bucket[agg.parentId] && bucket[agg.parentId].values; + const percentile = find(values, value => agg.key === value.key); + return percentile ? percentile.value : NaN; +} diff --git a/src/ui/public/agg_types/metrics/stdDeviation.js b/src/ui/public/agg_types/metrics/stdDeviation.js deleted file mode 100644 index 9a7a7ad27ddc5..0000000000000 --- a/src/ui/public/agg_types/metrics/stdDeviation.js +++ /dev/null @@ -1,61 +0,0 @@ -define(function (require) { - return function AggTypeMetricStandardDeviationProvider(Private) { - var _ = require('lodash'); - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); - var getResponseAggConfigClass = Private(require('ui/agg_types/metrics/getResponseAggConfigClass')); - - var responseAggConfigProps = { - valProp: function () { - var details = this.keyedDetails[this.key]; - return details.valProp; - }, - makeLabel: function () { - var details = this.keyedDetails[this.key]; - return details.title + ' of ' + this.fieldDisplayName(); - }, - keyedDetails: { - std_lower: { - valProp: ['std_deviation_bounds', 'lower'], - title: 'Lower Standard Deviation' - }, - avg: { - valProp: 'avg', - title: 'Average' - }, - std_upper: { - valProp: ['std_deviation_bounds', 'upper'], - title: 'Upper Standard Deviation' - } - } - }; - - return new MetricAggType({ - name: 'std_dev', - dslName: 'extended_stats', - title: 'Standard Deviation', - makeLabel: function (agg) { - return 'Standard Deviation of ' + agg.fieldDisplayName(); - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ], - - getResponseAggs: function (agg) { - var ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps); - - return [ - new ValueAggConfig('std_lower'), - new ValueAggConfig('avg'), - new ValueAggConfig('std_upper') - ]; - }, - - getValue: function (agg, bucket) { - return _.get(bucket[agg.parentId], agg.valProp()); - } - }); - }; -}); diff --git a/src/ui/public/agg_types/metrics/std_deviation.js b/src/ui/public/agg_types/metrics/std_deviation.js new file mode 100644 index 0000000000000..9982dd65a9a3c --- /dev/null +++ b/src/ui/public/agg_types/metrics/std_deviation.js @@ -0,0 +1,65 @@ +import _ from 'lodash'; +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; +import AggTypesMetricsGetResponseAggConfigClassProvider from 'ui/agg_types/metrics/get_response_agg_config_class'; +export default function AggTypeMetricStandardDeviationProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + let getResponseAggConfigClass = Private(AggTypesMetricsGetResponseAggConfigClassProvider); + + let responseAggConfigProps = { + valProp: function () { + let details = this.keyedDetails(this.params.customLabel)[this.key]; + return details.valProp; + }, + makeLabel: function () { + const fieldDisplayName = this.fieldDisplayName(); + const details = this.keyedDetails(this.params.customLabel, fieldDisplayName); + return _.get(details, [this.key, 'title']); + }, + keyedDetails: function (customLabel, fieldDisplayName) { + const label = customLabel ? customLabel : 'Standard Deviation of ' + fieldDisplayName; + return { + std_lower: { + valProp: ['std_deviation_bounds', 'lower'], + title: 'Lower ' + label + }, + avg: { + valProp: 'avg', + title: 'Average of ' + fieldDisplayName + }, + std_upper: { + valProp: ['std_deviation_bounds', 'upper'], + title: 'Upper ' + label + } + }; + } + }; + + return new MetricAggType({ + name: 'std_dev', + dslName: 'extended_stats', + title: 'Standard Deviation', + makeLabel: function (agg) { + return 'Standard Deviation of ' + agg.fieldDisplayName(); + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ], + + getResponseAggs: function (agg) { + let ValueAggConfig = getResponseAggConfigClass(agg, responseAggConfigProps); + + return [ + new ValueAggConfig('std_lower'), + new ValueAggConfig('avg'), + new ValueAggConfig('std_upper') + ]; + }, + + getValue: function (agg, bucket) { + return _.get(bucket[agg.parentId], agg.valProp()); + } + }); +}; diff --git a/src/ui/public/agg_types/metrics/sum.js b/src/ui/public/agg_types/metrics/sum.js index 5eced3aea75b8..d419edb241e65 100644 --- a/src/ui/public/agg_types/metrics/sum.js +++ b/src/ui/public/agg_types/metrics/sum.js @@ -1,19 +1,19 @@ -define(function (require) { - return function AggTypeMetricSumProvider(Private) { - var MetricAggType = Private(require('ui/agg_types/metrics/MetricAggType')); +import AggTypesMetricsMetricAggTypeProvider from 'ui/agg_types/metrics/metric_agg_type'; - return new MetricAggType({ - name: 'sum', - title: 'Sum', - makeLabel: function (aggConfig) { - return 'Sum of ' + aggConfig.params.field.displayName; - }, - params: [ - { - name: 'field', - filterFieldTypes: 'number' - } - ] - }); - }; -}); +export default function AggTypeMetricSumProvider(Private) { + let MetricAggType = Private(AggTypesMetricsMetricAggTypeProvider); + + return new MetricAggType({ + name: 'sum', + title: 'Sum', + makeLabel: function (aggConfig) { + return 'Sum of ' + aggConfig.params.field.displayName; + }, + params: [ + { + name: 'field', + filterFieldTypes: 'number' + } + ] + }); +}; diff --git a/src/ui/public/agg_types/param_types/base.js b/src/ui/public/agg_types/param_types/base.js index e0be2ae5b13e6..997ff7a8e4765 100644 --- a/src/ui/public/agg_types/param_types/base.js +++ b/src/ui/public/agg_types/param_types/base.js @@ -1,11 +1,9 @@ -define(function (require) { - return function BaseAggParamFactory() { - var _ = require('lodash'); +import _ from 'lodash'; +export default function BaseAggParamFactory() { - function BaseAggParam(config) { - _.assign(this, config); - } + function BaseAggParam(config) { + _.assign(this, config); + } - return BaseAggParam; - }; -}); + return BaseAggParam; +}; diff --git a/src/ui/public/agg_types/param_types/field.js b/src/ui/public/agg_types/param_types/field.js index 8a697da80f9a3..9a6fc8b369646 100644 --- a/src/ui/public/agg_types/param_types/field.js +++ b/src/ui/public/agg_types/param_types/field.js @@ -1,68 +1,69 @@ -define(function (require) { - return function FieldAggParamFactory(Private) { - var _ = require('lodash'); +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) { - var editorHtml = require('ui/agg_types/controls/field.html'); - var BaseAggParam = Private(require('ui/agg_types/param_types/base')); - var SavedObjectNotFound = require('ui/errors').SavedObjectNotFound; + let BaseAggParam = Private(AggTypesParamTypesBaseProvider); - _.class(FieldAggParam).inherits(BaseAggParam); - function FieldAggParam(config) { - FieldAggParam.Super.call(this, config); - } - - FieldAggParam.prototype.editor = editorHtml; - FieldAggParam.prototype.scriptable = false; - FieldAggParam.prototype.filterFieldTypes = '*'; + _.class(FieldAggParam).inherits(BaseAggParam); + function FieldAggParam(config) { + FieldAggParam.Super.call(this, config); + } - /** - * Called to serialize values for saving an aggConfig object - * - * @param {field} field - the field that was selected - * @return {string} - */ - FieldAggParam.prototype.serialize = function (field) { - return field.name; - }; + FieldAggParam.prototype.editor = editorHtml; + FieldAggParam.prototype.scriptable = false; + FieldAggParam.prototype.filterFieldTypes = '*'; - /** - * Called to read values from a database record into the - * aggConfig object - * - * @param {string} fieldName - * @return {field} - */ - FieldAggParam.prototype.deserialize = function (fieldName, aggConfig) { - var field = aggConfig.vis.indexPattern.fields.byName[fieldName]; + /** + * Called to serialize values for saving an aggConfig object + * + * @param {field} field - the field that was selected + * @return {string} + */ + FieldAggParam.prototype.serialize = function (field) { + return field.name; + }; - if (!field) { - throw new SavedObjectNotFound('index-pattern-field', fieldName); - } + /** + * Called to read values from a database record into the + * aggConfig object + * + * @param {string} fieldName + * @return {field} + */ + FieldAggParam.prototype.deserialize = function (fieldName, aggConfig) { + let field = aggConfig.vis.indexPattern.fields.byName[fieldName]; - return field; - }; + if (!field) { + throw new SavedObjectNotFound('index-pattern-field', fieldName); + } - /** - * Write the aggregation parameter. - * - * @param {AggConfig} aggConfig - the entire configuration for this agg - * @param {object} output - the result of calling write on all of the aggregations - * parameters. - * @param {object} output.params - the final object that will be included as the params - * for the agg - * @return {undefined} - */ - FieldAggParam.prototype.write = function (aggConfig, output) { - var field = aggConfig.params.field; + return field; + }; - if (field.scripted) { - output.params.script = field.script; - output.params.lang = field.lang; - } else { - output.params.field = field.name; - } - }; + /** + * Write the aggregation parameter. + * + * @param {AggConfig} aggConfig - the entire configuration for this agg + * @param {object} output - the result of calling write on all of the aggregations + * parameters. + * @param {object} output.params - the final object that will be included as the params + * for the agg + * @return {undefined} + */ + FieldAggParam.prototype.write = function (aggConfig, output) { + let field = aggConfig.params.field; - return FieldAggParam; + if (field.scripted) { + output.params.script = { + script: field.script, + lang: field.lang, + }; + } else { + output.params.field = field.name; + } }; -}); + + return FieldAggParam; +}; diff --git a/src/ui/public/agg_types/param_types/optioned.js b/src/ui/public/agg_types/param_types/optioned.js index 5d42ddf4e89bd..2e161a879a6c5 100644 --- a/src/ui/public/agg_types/param_types/optioned.js +++ b/src/ui/public/agg_types/param_types/optioned.js @@ -1,55 +1,54 @@ -define(function (require) { - return function OptionedAggParamFactory(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import IndexedArray from 'ui/indexed_array'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +export default function OptionedAggParamFactory(Private) { - var IndexedArray = require('ui/IndexedArray'); - var BaseAggParam = Private(require('ui/agg_types/param_types/base')); + let BaseAggParam = Private(AggTypesParamTypesBaseProvider); - _.class(OptionedAggParam).inherits(BaseAggParam); - function OptionedAggParam(config) { - OptionedAggParam.Super.call(this, config); + _.class(OptionedAggParam).inherits(BaseAggParam); + function OptionedAggParam(config) { + OptionedAggParam.Super.call(this, config); - this.options = new IndexedArray({ - index: ['val'], - immutable: true, - initialSet: this.options - }); - } + this.options = new IndexedArray({ + index: ['val'], + immutable: true, + initialSet: this.options + }); + } - /** - * Serialize a selection to be stored in the database - * @param {object} selected - the option that was selected - * @return {any} - */ - OptionedAggParam.prototype.serialize = function (selected) { - return selected.val; - }; - - /** - * Take a value that was serialized to the database and - * return the option that is represents - * - * @param {any} val - the value that was saved - * @return {object} - */ - OptionedAggParam.prototype.deserialize = function (val) { - return this.options.byVal[val]; - }; + /** + * Serialize a selection to be stored in the database + * @param {object} selected - the option that was selected + * @return {any} + */ + OptionedAggParam.prototype.serialize = function (selected) { + return selected.val; + }; - /** - * Write the aggregation parameter. - * - * @param {AggConfig} aggConfig - the entire configuration for this agg - * @param {object} output - the result of calling write on all of the aggregations - * parameters. - * @param {object} output.params - the final object that will be included as the params - * for the agg - * @return {undefined} - */ - OptionedAggParam.prototype.write = function (aggConfig, output) { - output.params[this.name] = aggConfig.params[this.name].val; - }; + /** + * Take a value that was serialized to the database and + * return the option that is represents + * + * @param {any} val - the value that was saved + * @return {object} + */ + OptionedAggParam.prototype.deserialize = function (val) { + return this.options.byVal[val]; + }; - return OptionedAggParam; + /** + * Write the aggregation parameter. + * + * @param {AggConfig} aggConfig - the entire configuration for this agg + * @param {object} output - the result of calling write on all of the aggregations + * parameters. + * @param {object} output.params - the final object that will be included as the params + * for the agg + * @return {undefined} + */ + OptionedAggParam.prototype.write = function (aggConfig, output) { + output.params[this.name] = aggConfig.params[this.name].val; }; -}); + + return OptionedAggParam; +}; diff --git a/src/ui/public/agg_types/param_types/raw_json.js b/src/ui/public/agg_types/param_types/raw_json.js index c338ee8feec34..25f160174230d 100644 --- a/src/ui/public/agg_types/param_types/raw_json.js +++ b/src/ui/public/agg_types/param_types/raw_json.js @@ -1,78 +1,77 @@ -define(function (require) { - return function RawJSONAggParamFactory(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import editorHtml from 'ui/agg_types/controls/raw_json.html'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +export default function RawJSONAggParamFactory(Private) { - var BaseAggParam = Private(require('ui/agg_types/param_types/base')); - var editorHtml = require('ui/agg_types/controls/raw_json.html'); + let BaseAggParam = Private(AggTypesParamTypesBaseProvider); - _.class(RawJSONAggParam).inherits(BaseAggParam); - function RawJSONAggParam(config) { - // force name override - config = _.defaults(config, { name: 'json' }); - RawJSONAggParam.Super.call(this, config); - } - - RawJSONAggParam.prototype.editor = editorHtml; + _.class(RawJSONAggParam).inherits(BaseAggParam); + function RawJSONAggParam(config) { + // force name override + config = _.defaults(config, { name: 'json' }); + RawJSONAggParam.Super.call(this, config); + } - /** - * Write the aggregation parameter. - * - * @param {AggConfig} aggConfig - the entire configuration for this agg - * @param {object} output - the result of calling write on all of the aggregations - * parameters. - * @param {object} output.params - the final object that will be included as the params - * for the agg - * @return {undefined} - */ - RawJSONAggParam.prototype.write = function (aggConfig, output) { - var paramJSON; - var param = aggConfig.params[this.name]; + RawJSONAggParam.prototype.editor = editorHtml; - if (!param) { - return; - } + /** + * Write the aggregation parameter. + * + * @param {AggConfig} aggConfig - the entire configuration for this agg + * @param {object} output - the result of calling write on all of the aggregations + * parameters. + * @param {object} output.params - the final object that will be included as the params + * for the agg + * @return {undefined} + */ + RawJSONAggParam.prototype.write = function (aggConfig, output) { + let paramJSON; + let param = aggConfig.params[this.name]; - // handle invalid JSON input - try { - paramJSON = JSON.parse(param); - } catch (err) { - return; - } + if (!param) { + return; + } - function filteredCombine(srcA, srcB) { - function mergeObjs(a, b) { - return _(a) - .keys() - .union(_.keys(b)) - .transform(function (dest, key) { - var val = compare(a[key], b[key]); - if (val !== undefined) dest[key] = val; - }, {}) - .value(); - } + // handle invalid JSON input + try { + paramJSON = JSON.parse(param); + } catch (err) { + return; + } - function mergeArrays(a, b) { - // attempt to merge each value - return _.times(Math.max(a.length, b.length), function (i) { - return compare(a[i], b[i]); - }); - } + function filteredCombine(srcA, srcB) { + function mergeObjs(a, b) { + return _(a) + .keys() + .union(_.keys(b)) + .transform(function (dest, key) { + let val = compare(a[key], b[key]); + if (val !== undefined) dest[key] = val; + }, {}) + .value(); + } - function compare(a, b) { - if (_.isPlainObject(a) && _.isPlainObject(b)) return mergeObjs(a, b); - if (_.isArray(a) && _.isArray(b)) return mergeArrays(a, b); - if (b === null) return undefined; - if (b !== undefined) return b; - return a; - } + function mergeArrays(a, b) { + // attempt to merge each value + return _.times(Math.max(a.length, b.length), function (i) { + return compare(a[i], b[i]); + }); + } - return compare(srcA, srcB); + function compare(a, b) { + if (_.isPlainObject(a) && _.isPlainObject(b)) return mergeObjs(a, b); + if (_.isArray(a) && _.isArray(b)) return mergeArrays(a, b); + if (b === null) return undefined; + if (b !== undefined) return b; + return a; } - output.params = filteredCombine(output.params, paramJSON); - return; - }; + return compare(srcA, srcB); + } - return RawJSONAggParam; + output.params = filteredCombine(output.params, paramJSON); + return; }; -}); + + return RawJSONAggParam; +}; diff --git a/src/ui/public/agg_types/param_types/regex.js b/src/ui/public/agg_types/param_types/regex.js index ea1a3165b5c40..3a38c06f8b9da 100644 --- a/src/ui/public/agg_types/param_types/regex.js +++ b/src/ui/public/agg_types/param_types/regex.js @@ -1,71 +1,52 @@ -define(function (require) { - return function RegexAggParamFactory(Private) { - var _ = require('lodash'); - - var BaseAggParam = Private(require('ui/agg_types/param_types/base')); - var editorHtml = require('ui/agg_types/controls/regular_expression.html'); - - _.class(RegexAggParam).inherits(BaseAggParam); - function RegexAggParam(config) { - // Java RegExp flags - var flags = [ - 'CANON_EQ', - 'CASE_INSENSITIVE', - 'COMMENTS', - 'DOTALL', - 'LITERAL', - 'MULTILINE', - 'UNICODE_CASE', - 'UNICODE_CHARACTER_CLASS', - 'UNIX_LINES' - ]; +import _ from 'lodash'; +import editorHtml from 'ui/agg_types/controls/regular_expression.html'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +export default function RegexAggParamFactory(Private) { + + let BaseAggParam = Private(AggTypesParamTypesBaseProvider); + + _.class(RegexAggParam).inherits(BaseAggParam); + function RegexAggParam(config) { + _.defaults(config, { pattern: '' }); + RegexAggParam.Super.call(this, config); + } + + RegexAggParam.prototype.editor = editorHtml; + + /** + * Disabled state of the agg param + * + * @return {bool} + */ + RegexAggParam.prototype.disabled = function (aggConfig) { + return false; + }; - _.defaults(config, { pattern: '', flags: flags }); - RegexAggParam.Super.call(this, config); + /** + * Write the aggregation parameter. + * + * @param {AggConfig} aggConfig - the entire configuration for this agg + * @param {object} output - the result of calling write on all of the aggregations + * parameters. + * @param {object} output.params - the final object that will be included as the params + * for the agg + * @return {undefined} + */ + RegexAggParam.prototype.write = function (aggConfig, output) { + let param = aggConfig.params[this.name]; + let paramType = aggConfig.type.params.byName[this.name]; + + // clear aggParam if pattern is not set or is disabled + if (!param || !param.pattern || !param.pattern.length || paramType.disabled(aggConfig)) { + return; } - RegexAggParam.prototype.editor = editorHtml; - - /** - * Disabled state of the agg param - * - * @return {bool} - */ - RegexAggParam.prototype.disabled = function (aggConfig) { - return false; + let obj = { + pattern: param.pattern }; - /** - * Write the aggregation parameter. - * - * @param {AggConfig} aggConfig - the entire configuration for this agg - * @param {object} output - the result of calling write on all of the aggregations - * parameters. - * @param {object} output.params - the final object that will be included as the params - * for the agg - * @return {undefined} - */ - RegexAggParam.prototype.write = function (aggConfig, output) { - var param = aggConfig.params[this.name]; - var paramType = aggConfig.type.params.byName[this.name]; - - // clear aggParam if pattern is not set or is disabled - if (!param || !param.pattern || !param.pattern.length || paramType.disabled(aggConfig)) { - return; - } - - var obj = { - pattern: param.pattern - }; - - // include any selected flags - if (_.isArray(param.flags) && param.flags.length) { - obj.flags = param.flags.join('|'); - } - - output.params[this.name] = obj; - }; - - return RegexAggParam; + output.params[this.name] = obj; }; -}); + + return RegexAggParam; +}; diff --git a/src/ui/public/agg_types/param_types/string.js b/src/ui/public/agg_types/param_types/string.js index e6135741e8971..58803171c44a4 100644 --- a/src/ui/public/agg_types/param_types/string.js +++ b/src/ui/public/agg_types/param_types/string.js @@ -1,33 +1,32 @@ -define(function (require) { - return function FieldAggParamFactory(Private) { - var _ = require('lodash'); +import _ from 'lodash'; +import editorHtml from 'ui/agg_types/controls/string.html'; +import AggTypesParamTypesBaseProvider from 'ui/agg_types/param_types/base'; +export default function FieldAggParamFactory(Private) { - var editorHtml = require('ui/agg_types/controls/string.html'); - var BaseAggParam = Private(require('ui/agg_types/param_types/base')); + let BaseAggParam = Private(AggTypesParamTypesBaseProvider); - _.class(ScriptAggParam).inherits(BaseAggParam); - function ScriptAggParam(config) { - ScriptAggParam.Super.call(this, config); - } - - ScriptAggParam.prototype.editor = editorHtml; + _.class(ScriptAggParam).inherits(BaseAggParam); + function ScriptAggParam(config) { + ScriptAggParam.Super.call(this, config); + } - /** - * Write the aggregation parameter. - * - * @param {AggConfig} aggConfig - the entire configuration for this agg - * @param {object} output - the result of calling write on all of the aggregations - * parameters. - * @param {object} output.params - the final object that will be included as the params - * for the agg - * @return {undefined} - */ - ScriptAggParam.prototype.write = function (aggConfig, output) { - if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { - output.params[this.name] = aggConfig.params[this.name]; - } - }; + ScriptAggParam.prototype.editor = editorHtml; - return ScriptAggParam; + /** + * Write the aggregation parameter. + * + * @param {AggConfig} aggConfig - the entire configuration for this agg + * @param {object} output - the result of calling write on all of the aggregations + * parameters. + * @param {object} output.params - the final object that will be included as the params + * for the agg + * @return {undefined} + */ + ScriptAggParam.prototype.write = function (aggConfig, output) { + if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { + output.params[this.name] = aggConfig.params[this.name]; + } }; -}); + + return ScriptAggParam; +}; diff --git a/src/ui/public/autoload/all.js b/src/ui/public/autoload/all.js new file mode 100644 index 0000000000000..98b0b4961154c --- /dev/null +++ b/src/ui/public/autoload/all.js @@ -0,0 +1,4 @@ +import './modules'; +import './directives'; +import './filters'; +import './styles'; diff --git a/src/ui/public/autoload/directives.js b/src/ui/public/autoload/directives.js new file mode 100644 index 0000000000000..3908c3e116de1 --- /dev/null +++ b/src/ui/public/autoload/directives.js @@ -0,0 +1,2 @@ +const context = require.context('../directives', false, /[\/\\](?!\.|_)[^\/\\]+\.js/); +context.keys().forEach(key => context(key)); diff --git a/src/ui/public/autoload/filters.js b/src/ui/public/autoload/filters.js new file mode 100644 index 0000000000000..583d1dcead072 --- /dev/null +++ b/src/ui/public/autoload/filters.js @@ -0,0 +1,2 @@ +const context = require.context('../filters', false, /[\/\\](?!\.|_)[^\/\\]+\.js/); +context.keys().forEach(key => context(key)); diff --git a/src/ui/public/autoload/modules.js b/src/ui/public/autoload/modules.js new file mode 100644 index 0000000000000..aae542cf95911 --- /dev/null +++ b/src/ui/public/autoload/modules.js @@ -0,0 +1,35 @@ +import 'angular'; +import 'ui/chrome'; +import 'ui/bind'; +import 'ui/kbn_top_nav'; +import 'ui/bound_to_config_obj'; +import 'ui/config'; +import 'ui/courier'; +import 'ui/debounce'; +import 'ui/doc_title'; +import 'ui/elastic_textarea'; +import 'ui/es'; +import 'ui/events'; +import 'ui/fancy_forms'; +import 'ui/filter_bar'; +import 'ui/filter_manager'; +import 'ui/index_patterns'; +import 'ui/listen'; +import 'ui/notify'; +import 'ui/parse_query'; +import 'ui/persisted_log'; +import 'ui/private'; +import 'ui/promises'; +import 'ui/safe_confirm'; +import 'ui/state_management/app_state'; +import 'ui/state_management/global_state'; +import 'ui/storage'; +import 'ui/stringify/register'; +import 'ui/style_compile'; +import 'ui/timefilter'; +import 'ui/timepicker'; +import 'ui/tooltip'; +import 'ui/typeahead'; +import 'ui/url'; +import 'ui/validate_date_interval'; +import 'ui/watch_multi'; diff --git a/src/ui/public/autoload/styles.js b/src/ui/public/autoload/styles.js new file mode 100644 index 0000000000000..040094a9ce921 --- /dev/null +++ b/src/ui/public/autoload/styles.js @@ -0,0 +1,2 @@ +const context = require.context('../styles', false, /[\/\\](?!mixins|variables|_|\.)[^\/\\]+\.less/); +context.keys().forEach(key => context(key)); diff --git a/src/ui/public/bind/__tests__/bind.js b/src/ui/public/bind/__tests__/bind.js index 34fbbbd395f19..e7acb9b037080 100644 --- a/src/ui/public/bind/__tests__/bind.js +++ b/src/ui/public/bind/__tests__/bind.js @@ -1,10 +1,10 @@ +import sinon from 'auto-release-sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; describe('$scope.$bind', function () { - var sinon = require('auto-release-sinon'); - var expect = require('expect.js'); - var ngMock = require('ngMock'); - var $rootScope; - var $scope; + let $rootScope; + let $scope; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function ($injector) { @@ -16,7 +16,7 @@ describe('$scope.$bind', function () { expect($rootScope.$bind).to.be.a('function'); expect($scope).to.have.property('$bind', $rootScope.$bind); - var $isoScope = $scope.$new(true); + let $isoScope = $scope.$new(true); expect($isoScope).to.have.property('$bind', $rootScope.$bind); }); @@ -33,7 +33,7 @@ describe('$scope.$bind', function () { }); it('sets up a binding from the child to the parent scope', function () { - var stub = sinon.stub(); + let stub = sinon.stub(); $rootScope.val = 'foo'; $scope.$bind('localVal', 'val'); @@ -47,8 +47,8 @@ describe('$scope.$bind', function () { }); it('pulls from the scopes $parent by default', function () { - var $parent = $rootScope.$new(); - var $self = $parent.$new(); + let $parent = $rootScope.$new(); + let $self = $parent.$new(); $parent.val = 'foo'; $self.val = 'bar'; @@ -58,8 +58,8 @@ describe('$scope.$bind', function () { }); it('accepts an alternate scope to read from', function () { - var $parent = $rootScope.$new(); - var $self = $parent.$new(); + let $parent = $rootScope.$new(); + let $self = $parent.$new(); $parent.val = 'foo'; $self.val = 'bar'; diff --git a/src/ui/public/bind/bind.js b/src/ui/public/bind/bind.js index fe229ad68adef..ba16ddc5bfdcb 100644 --- a/src/ui/public/bind/bind.js +++ b/src/ui/public/bind/bind.js @@ -1,82 +1,81 @@ -define(function (require) { - var _ = require('lodash'); - var angular = require('angular'); - - require('ui/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 - * allow values on scope that work like they do in an isolate scope, but - * without requiring one. - * - * @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, $sourceScope) { - var $source = $sourceScope || this.$parent; - var $target = this; - - // parse expressions - var $to = $parse(to); - if (!$to.assign) errorNotAssignable(to, from); - var $from = $parse(from); - - // 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); }; - - // 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(); - - $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); - }; - - // if we are syncing down a literal, then we use loose equality check - var strict = !$from.literal; - var compare = strict ? strictEquality : angular.equals; - - - // 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); +import _ from 'lodash'; +import angular from 'angular'; +import uiModules from 'ui/modules'; + +uiModules.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 + * allow values on scope that work like they do in an isolate scope, but + * without requiring one. + * + * @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, $sourceScope) { + let $source = $sourceScope || this.$parent; + let $target = this; + + // parse expressions + let $to = $parse(to); + if (!$to.assign) errorNotAssignable(to, from); + let $from = $parse(from); + + // bind scopes to expressions + let getTarget = function () { return $to($target); }; + let setTarget = function (v) { return $to.assign($target, v); }; + let getSource = function () { return $from($source); }; + let setSource = function (v) { return $from.assignOrFail($source, v); }; + + // 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 + let lastSourceVal = getSource(); + + $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); }; - return $delegate; - }); + // if we are syncing down a literal, then we use loose equality check + let strict = !$from.literal; + let compare = strict ? strictEquality : angular.equals; + + + // push the initial value down, start off in sync + setTarget(lastSourceVal); + + $target.$watch(function () { + let sourceVal = getSource(); + let targetVal = getTarget(); + + let outOfSync = !compare(sourceVal, targetVal); + let sourceChanged = outOfSync && !compare(sourceVal, lastSourceVal); + + if (sourceChanged) setTarget(sourceVal); + else if (outOfSync) setSource(targetVal); + + return lastSourceVal = sourceVal; + }, null, !strict); + }; + + return $delegate; }); }); diff --git a/src/ui/public/binder/__tests__/binder.js b/src/ui/public/binder/__tests__/binder.js new file mode 100644 index 0000000000000..182050bb597fd --- /dev/null +++ b/src/ui/public/binder/__tests__/binder.js @@ -0,0 +1,67 @@ +import sinon from 'auto-release-sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import Binder from 'ui/binder'; +import $ from 'jquery'; + +describe('Binder class', function () { + let $scope; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function ($rootScope) { + $scope = $rootScope.$new(); + })); + + context('Constructing with a $scope', function () { + it('accepts a $scope and listens for $destroy', function () { + sinon.stub($scope, '$on'); + let binder = new Binder($scope); + expect($scope.$on.callCount).to.be(1); + expect($scope.$on.args[0][0]).to.be('$destroy'); + }); + + it('unbinds when the $scope is destroyed', function () { + let binder = new Binder($scope); + sinon.stub(binder, 'destroy'); + $scope.$destroy(); + expect(binder.destroy.callCount).to.be(1); + }); + }); + + describe('Binder#on', function () { + it('binds to normal event emitters', function () { + let binder = new Binder(); + let emitter = { + on: sinon.stub(), + removeListener: sinon.stub() + }; + let handler = sinon.stub(); + + binder.on(emitter, 'click', handler); + expect(emitter.on.callCount).to.be(1); + expect(emitter.on.args[0][0]).to.be('click'); + expect(emitter.on.args[0][1]).to.be(handler); + + binder.destroy(); + expect(emitter.removeListener.callCount).to.be(1); + expect(emitter.removeListener.args[0][0]).to.be('click'); + expect(emitter.removeListener.args[0][1]).to.be(handler); + }); + }); + + describe('Binder#jqOn', function () { + it('binds jquery event handlers', function () { + let binder = new Binder(); + let el = document.createElement('div'); + let handler = sinon.stub(); + + binder.jqOn(el, 'click', handler); + $(el).click(); + expect(handler.callCount).to.be(1); + binder.destroy(); + $(el).click(); + expect(handler.callCount).to.be(1); + }); + }); +}); diff --git a/src/ui/public/binder/binder.js b/src/ui/public/binder/binder.js new file mode 100644 index 0000000000000..90f27515959f7 --- /dev/null +++ b/src/ui/public/binder/binder.js @@ -0,0 +1,34 @@ +import d3 from 'd3'; +import $ from 'jquery'; + +import Binder from '../../../utils/binder'; + +export default class UiBinder extends Binder { + constructor($scope) { + super(); + + // support auto-binding to $scope objects + if ($scope) { + $scope.$on('$destroy', () => this.destroy()); + } + } + + jqOn(el, ...args) { + const $el = $(el); + $el.on(...args); + this.disposal.push(() => $el.off(...args)); + } + + fakeD3Bind(el, event, handler) { + this.jqOn(el, event, (e) => { + // mimick https://github.com/mbostock/d3/blob/3abb00113662463e5c19eb87cd33f6d0ddc23bc0/src/selection/on.js#L87-L94 + const o = d3.event; // Events can be reentrant (e.g., focus). + d3.event = e; + try { + handler.apply(this, [this.__data__]); + } finally { + d3.event = o; + } + }); + } +} diff --git a/src/ui/public/bound_to_config_obj.js b/src/ui/public/bound_to_config_obj.js index a16487835a6c1..5f4ac5272dde8 100644 --- a/src/ui/public/bound_to_config_obj.js +++ b/src/ui/public/bound_to_config_obj.js @@ -1,43 +1,37 @@ -define(function (require) { - return function BoundToConfigObjProvider($rootScope, config) { - var _ = require('lodash'); +import _ from 'lodash'; +export default function BoundToConfigObjProvider(config) { - /** - * 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; + /** + * 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) { + const self = this; - _.forOwn(input, function (val, prop) { - if (!_.isString(val) || val.charAt(0) !== '=') { - self[prop] = val; - return; - } + _.forOwn(input, function (value, prop) { + if (!_.isString(value) || value.charAt(0) !== '=') { + self[prop] = value; + 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); - } + const configKey = value.substr(1); + config.watch(configKey, function update(value) { + self[prop] = value; }); - } + }); + } - return BoundToConfigObj; + return BoundToConfigObj; - }; -}); +}; diff --git a/src/ui/public/chrome/Tab.js b/src/ui/public/chrome/Tab.js deleted file mode 100644 index cceb65008087f..0000000000000 --- a/src/ui/public/chrome/Tab.js +++ /dev/null @@ -1,71 +0,0 @@ -const _ = require('lodash'); -const reEsc = require('lodash').escapeRegExp; -const { parse, format } = require('url'); - -const urlJoin = (a, b) => { - if (!b) return a; - return `${a}${ a.endsWith('/') ? '' : '/' }${b}`; -}; - -export default class Tab { - constructor(spec = {}) { - this.id = spec.id || ''; - this.title = spec.title || ''; - this.resetWhenActive = !!spec.resetWhenActive; - this.activeIndicatorColor = spec.activeIndicatorColor || null; - if (_.isFunction(this.activeIndicatorColor)) { - // convert to a getter - Object.defineProperty(this, 'activeIndicatorColor', { - get: this.activeIndicatorColor - }); - } - - this.active = false; - - this.baseUrl = spec.baseUrl || '/'; - this.rootUrl = urlJoin(this.baseUrl, this.id); - this.rootRegExp = new RegExp(`^${reEsc(this.rootUrl)}(/|$|\\?|#)`); - - this.lastUrlStoreKey = `lastUrl:${this.id}`; - this.lastUrlStore = spec.lastUrlStore; - this.lastUrl = this.lastUrlStore ? this.lastUrlStore.getItem(this.lastUrlStoreKey) : null; - } - - href() { - if (this.active) { - return this.resetWhenActive ? this.rootUrl : null; - } - return this.lastUrl || this.rootUrl; - } - - updateLastUrlGlobalState(globalState) { - let lastPath = this.getLastPath(); - let { pathname, query, hash } = parse(lastPath, true); - - query = query || {}; - if (!globalState) delete query._g; - else query._g = globalState; - - this.setLastUrl(`${this.rootUrl}${format({ pathname, query, hash })}`); - } - - getLastPath() { - let { id, rootUrl } = this; - let lastUrl = this.getLastUrl(); - - if (!lastUrl.startsWith(rootUrl)) { - throw new Error(`Tab "${id}" has invalid root "${rootUrl}" for last url "${lastUrl}"`); - } - - return lastUrl.slice(rootUrl.length); - } - - setLastUrl(url) { - this.lastUrl = url; - if (this.lastUrlStore) this.lastUrlStore.setItem(this.lastUrlStoreKey, this.lastUrl); - } - - getLastUrl() { - return this.lastUrl || this.rootUrl; - } -} diff --git a/src/ui/public/chrome/TabCollection.js b/src/ui/public/chrome/TabCollection.js deleted file mode 100644 index 7287d1a0e7b6b..0000000000000 --- a/src/ui/public/chrome/TabCollection.js +++ /dev/null @@ -1,53 +0,0 @@ -var _ = require('lodash'); -var { startsWith, get, set, omit, wrap, pick } = require('lodash'); -var Tab = require('ui/chrome/Tab'); -var { parse } = require('url'); - -function TabCollection(opts = {}) { - var tabs = []; - var specs = null; - var defaults = opts.defaults || {}; - var activeTab = null; - - this.set = function (_specs) { - specs = _.cloneDeep([].concat(_specs || [])); - this._rebuildTabs(); - }; - - this.setDefaults = function () { - defaults = _.defaults({}, arguments[0], defaults); - this._rebuildTabs(); - }; - - this.get = function () { - return [].concat(tabs || []); - }; - - this._rebuildTabs = function () { - _.invoke(this.get(), 'destroy'); - tabs = _.map(specs, function (spec) { - return new Tab(_.defaults({}, spec, defaults)); - }); - }; - - this.getActive = function () { - return activeTab; - }; - - this.consumeRouteUpdate = function (href, persist) { - tabs.forEach(function (tab) { - tab.active = tab.rootRegExp.test(href); - if (tab.active) { - activeTab = tab; - activeTab.setLastUrl(href); - } - }); - - if (!persist || !activeTab) return; - - let globalState = get(parse(activeTab.getLastPath(), true), 'query._g'); - tabs.forEach(tab => tab.updateLastUrlGlobalState(globalState)); - }; -} - -module.exports = TabCollection; diff --git a/src/ui/public/chrome/__tests__/Tab.js b/src/ui/public/chrome/__tests__/Tab.js deleted file mode 100644 index b25995730ec20..0000000000000 --- a/src/ui/public/chrome/__tests__/Tab.js +++ /dev/null @@ -1,241 +0,0 @@ -const Tab = require('../Tab'); -const expect = require('expect.js'); -const TabFakeStore = require('./_TabFakeStore'); - -describe('Chrome Tab', function () { - describe('construction', function () { - it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor, baseUrl', function () { - const tab = new Tab({ - id: 'foo', - title: 'Foo App', - resetWhenActive: false, - activeIndicatorColor: true, - baseUrl: 'proto:host.domain:999' - }); - - expect(tab.id).to.equal('foo'); - expect(tab.title).to.equal('Foo App'); - expect(tab.resetWhenActive).to.equal(false); - expect(tab.activeIndicatorColor).to.equal(true); - expect(tab.rootUrl).to.equal('proto:host.domain:999/foo'); - - const tab2 = new Tab({ - id: 'bar', - title: 'Bar App', - resetWhenActive: true, - activeIndicatorColor: false, - baseUrl: 'proto:host.domain:999/sub/#/' - }); - - expect(tab2.id).to.equal('bar'); - expect(tab2.title).to.equal('Bar App'); - expect(tab2.resetWhenActive).to.equal(true); - expect(tab2.activeIndicatorColor).to.equal(null); - expect(tab2.rootUrl).to.equal('proto:host.domain:999/sub/#/bar'); - }); - - it('starts inactive', function () { - const tab = new Tab(); - expect(tab.active).to.equal(false); - }); - - it('uses the id to set the rootUrl', function () { - const id = 'foo'; - const tab = new Tab({ id }); - expect(tab.id).to.equal(id); - expect(tab.rootUrl).to.equal(`/${id}`); - }); - - it('creates a regexp for matching the rootUrl', function () { - const tab = new Tab({ id: 'foo' }); - - expect('/foo').to.match(tab.rootRegExp); - expect('/foo/bar').to.match(tab.rootRegExp); - expect('/foo/bar/max').to.match(tab.rootRegExp); - expect('/foo?bar=baz').to.match(tab.rootRegExp); - expect('/foo/?bar=baz').to.match(tab.rootRegExp); - expect('/foo#?bar=baz').to.match(tab.rootRegExp); - - expect('/foobar').to.not.match(tab.rootRegExp); - expect('site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); - expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); - }); - - it('includes the baseUrl in the rootRegExp if specified', function () { - const tab = new Tab({ - id: 'foo', - baseUrl: 'http://spiderman.com/kibana' - }); - - expect('http://spiderman.com/kibana/foo/bar').to.match(tab.rootRegExp); - - expect('/foo').to.not.match(tab.rootRegExp); - expect('https://spiderman.com/kibana/foo/bar').to.not.match(tab.rootRegExp); - }); - - it('accepts a function for activeIndicatorColor', function () { - let i = 0; - const tab = new Tab({ - activeIndicatorColor: function () { - return i++; - } - }); - expect(tab.activeIndicatorColor).to.equal(0); - expect(tab.activeIndicatorColor).to.equal(1); - expect(tab.activeIndicatorColor).to.equal(2); - expect(tab.activeIndicatorColor).to.equal(3); - }); - - it('discovers the lastUrl', function () { - const lastUrlStore = new TabFakeStore(); - const tab = new Tab({ id: 'foo', lastUrlStore }); - expect(tab.lastUrl).to.not.equal('bar'); - - tab.setLastUrl('bar'); - expect(tab.lastUrl).to.equal('bar'); - - const tab2 = new Tab({ id: 'foo', lastUrlStore }); - expect(tab2.lastUrl).to.equal('bar'); - }); - }); - - - describe('#setLastUrl()', function () { - it('updates the lastUrl and storage value if passed a lastUrlStore', function () { - const lastUrlStore = new TabFakeStore(); - const tab = new Tab({ id: 'foo', lastUrlStore }); - - expect(tab.lastUrl).to.not.equal('foo'); - tab.setLastUrl('foo'); - expect(tab.lastUrl).to.equal('foo'); - expect(lastUrlStore.getItem(tab.lastUrlStoreKey)).to.equal('foo'); - }); - - it('only updates lastUrl if no lastUrlStore', function () { - const tab = new Tab({ id: 'foo' }); - - expect(tab.lastUrl).to.equal(null); - tab.setLastUrl('foo'); - expect(tab.lastUrl).to.equal('foo'); - - const tab2 = new Tab({ id: 'foo' }); - expect(tab2.lastUrl).to.not.equal('foo'); - }); - }); - - - describe('#href()', function () { - it('returns the rootUrl/id be default', function () { - const tab = new Tab({ id: 'foo' }); - expect(tab.href()).to.equal(tab.rootUrl); - }); - - it('returns the lastUrl if tracking is on', function () { - const tab = new Tab({ id: 'foo' }); - tab.setLastUrl('okay'); - expect(tab.href()).to.equal('okay'); - }); - - describe('when the tab is active', function () { - it('returns the rootUrl when resetWhenActive: true', function () { - const id = 'foo'; - const resetWhenActive = true; - const tab = new Tab({ id, resetWhenActive }); - - tab.active = true; - - expect(tab.href()).to.not.equal('butt'); - expect(tab.href()).to.equal(tab.rootUrl); - }); - - it('or returns null when not', function () { - const tab = new Tab({ id: 'foo', resetWhenActive: false }); - tab.active = true; - - expect(tab.href()).to.not.equal('butt'); - expect(tab.href()).to.equal(null); - }); - }); - }); - - describe('#getLastPath()', function () { - it('parses a path out of the lastUrl by removing the baseUrl', function () { - const baseUrl = 'http://local:5601/app/visualize#'; - const tab = new Tab({ baseUrl }); - - tab.setLastUrl('http://local:5601/app/visualize#/index'); - expect(tab.getLastPath()).to.equal('/index'); - }); - - it('throws an error if the lastUrl does not extend the root url', function () { - expect(function () { - const baseUrl = 'http://local:5601/app/visualize#'; - const tab = new Tab({ baseUrl }); - - tab.setLastUrl('http://local:5601/'); - tab.getLastPath(); - }).to.throwError(/invalid.*root/); - }); - }); - - describe('updateLastUrlGlobalState', function () { - const bases = [ - 'http://local:5601', - '', - 'weird.domain/with/subpath?path#', - 'weird.domain/with/#hashpath', - ]; - - context('with new state sets _g properly', function () { - const paths = [ - [ '/', '/?_g=newState' ], - [ '/?first', '/?first=&_g=newState' ], - [ '/path?first=1&_g=afterHash', '/path?first=1&_g=newState' ], - [ '/?first=1&_g=second', '/?first=1&_g=newState' ], - [ '/?g=first', '/?g=first&_g=newState' ], - [ '/a?first=1&_g=second', '/a?first=1&_g=newState' ], - [ '/?first=1&_g=second', '/?first=1&_g=newState' ], - [ '/?first&g=second', '/?first=&g=second&_g=newState' ], - ]; - - bases.forEach(baseUrl => { - paths.forEach(([pathFrom, pathTo]) => { - const fromUrl = `${baseUrl}${pathFrom}`; - const toUrl = `${baseUrl}${pathTo}`; - it(`${fromUrl} => ${toUrl}`, function () { - const tab = new Tab({ baseUrl }); - tab.setLastUrl(fromUrl); - tab.updateLastUrlGlobalState('newState'); - expect(tab.getLastUrl()).to.equal(toUrl); - }); - }); - }); - }); - - context('with new empty state removes _g', function () { - const paths = [ - [ '/', '/' ], - [ '/?first', '/?first=' ], - [ '/path?first=1&_g=afterHash', '/path?first=1' ], - [ '/?first=1&_g=second', '/?first=1' ], - [ '/?g=first', '/?g=first' ], - [ '/a?first=1&_g=second', '/a?first=1' ], - [ '/?first=1&_g=second', '/?first=1' ], - [ '/?first&g=second', '/?first=&g=second' ], - ]; - - bases.forEach(baseUrl => { - paths.forEach(([pathFrom, pathTo]) => { - const fromUrl = `${baseUrl}${pathFrom}`; - const toUrl = `${baseUrl}${pathTo}`; - it(`${fromUrl}`, function () { - const tab = new Tab({ baseUrl }); - tab.setLastUrl(fromUrl); - tab.updateLastUrlGlobalState(); - expect(tab.getLastUrl()).to.equal(toUrl); - }); - }); - }); - }); - }); -}); diff --git a/src/ui/public/chrome/__tests__/TabCollection.js b/src/ui/public/chrome/__tests__/TabCollection.js deleted file mode 100644 index fa12a4454fab8..0000000000000 --- a/src/ui/public/chrome/__tests__/TabCollection.js +++ /dev/null @@ -1,73 +0,0 @@ -const expect = require('expect.js'); -const { indexBy, random } = require('lodash'); - -const TabFakeStore = require('./_TabFakeStore'); -const TabCollection = require('../TabCollection'); -const Tab = require('../Tab'); - -describe('Chrome TabCollection', function () { - describe('empty state', function () { - it('has no tabs', function () { - const tabs = new TabCollection(); - expect(tabs.get()).to.eql([]); - }); - - it('has no active tab', function () { - const tabs = new TabCollection(); - expect(!tabs.getActive()).to.equal(true); - }); - }); - - describe('#set()', function () { - it('consumes an ordered list of Tab specs', function () { - const tabs = new TabCollection(); - tabs.set([ - { id: 'foo' }, - { id: 'bar' } - ]); - - const ts = tabs.get(); - expect(ts.length).to.equal(2); - expect(ts[0].id).to.equal('foo'); - expect(ts[1].id).to.equal('bar'); - }); - }); - - describe('#setDefaults()', function () { - it('applies the defaults used to create tabs', function () { - const tabs = new TabCollection(); - tabs.setDefaults({ id: 'thing' }); - tabs.set([ {} ]); - - expect(tabs.get()[0].id).to.equal('thing'); - }); - - it('recreates existing tabs with new defaults', function () { - const tabs = new TabCollection(); - tabs.set([ {} ]); - expect(!tabs.get()[0].id).to.equal(true); - - tabs.setDefaults({ id: 'thing' }); - expect(tabs.get()[0].id).to.equal('thing'); - }); - }); - - describe('#consumeRouteUpdate()', function () { - it('updates the active tab', function () { - const store = new TabFakeStore(); - const baseUrl = `http://localhost:${random(1000, 9999)}`; - const tabs = new TabCollection({ store, defaults: { baseUrl } }); - tabs.set([ - { id: 'a' }, - { id: 'b' } - ]); - - tabs.consumeRouteUpdate(`${baseUrl}/a`); - const {a, b} = indexBy(tabs.get(), 'id'); - expect(a.active).to.equal(true); - expect(b.active).to.equal(false); - expect(tabs.getActive()).to.equal(a); - }); - }); - -}); diff --git a/src/ui/public/chrome/__tests__/_TabFakeStore.js b/src/ui/public/chrome/__tests__/_TabFakeStore.js deleted file mode 100644 index 1b8ab81602259..0000000000000 --- a/src/ui/public/chrome/__tests__/_TabFakeStore.js +++ /dev/null @@ -1,9 +0,0 @@ -const store = Symbol('store'); - -export default class TabFakeStore { - constructor() { this[store] = new Map(); } - getItem(k) { return this[store].get(k); } - setItem(k, v) { return this[store].set(k, v); } - getKeys() { return [ ...this[store].keys() ]; } - getValues() { return [ ...this[store].values() ]; } -} diff --git a/src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js b/src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js new file mode 100644 index 0000000000000..812cd85ce4d75 --- /dev/null +++ b/src/ui/public/chrome/__tests__/fixtures/stub_browser_storage.js @@ -0,0 +1,10 @@ +const store = Symbol('store'); + +export default class StubBrowserStorage { + constructor() { this[store] = new Map(); } + getItem(k) { return this[store].get(k); } + setItem(k, v) { return this[store].set(k, String(v)); } + removeItem(k) { return this[store].delete(k); } + getKeys() { return [ ...this[store].keys() ]; } + getValues() { return [ ...this[store].values() ]; } +} diff --git a/src/ui/public/chrome/__tests__/kbn_loading_indicator.js b/src/ui/public/chrome/__tests__/kbn_loading_indicator.js new file mode 100644 index 0000000000000..aad685d19d15d --- /dev/null +++ b/src/ui/public/chrome/__tests__/kbn_loading_indicator.js @@ -0,0 +1,36 @@ +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import uiModules from 'ui/modules'; +import $ from 'jquery'; + +import '../directives/kbn_loading_indicator'; + + +describe('kbnLoadingIndicator', function () { + let compile; + + beforeEach(() => { + ngMock.module('kibana'); + ngMock.inject(function ($compile, $rootScope) { + compile = function (hasActiveConnections) { + $rootScope.chrome = { + httpActive: (hasActiveConnections ? [1] : []) + }; + const $el = $(''); + $rootScope.$apply(); + $compile($el)($rootScope); + return $el; + }; + }); + + }); + + it('injects a loading .spinner into the element', function () { + const $el = compile(); + expect($el.find('.spinner')).to.have.length(1); + }); + it('applies removes ng-hide class when there are connections', function () { + const $el = compile(true); + expect($el.find('.spinner.ng-hide')).to.have.length(0); + }); +}); diff --git a/src/ui/public/chrome/__tests__/nav_controls.js b/src/ui/public/chrome/__tests__/nav_controls.js new file mode 100644 index 0000000000000..dda0bdc483465 --- /dev/null +++ b/src/ui/public/chrome/__tests__/nav_controls.js @@ -0,0 +1,73 @@ +import ngMock from 'ng_mock'; +import $ from 'jquery'; +import expect from 'expect.js'; + +import uiModules from 'ui/modules'; +import chromeNavControlsRegistry from 'ui/registry/chrome_nav_controls'; +import Registry from 'ui/registry/_registry'; + +describe('chrome nav controls', function () { + let compile; + let stubRegistry; + + beforeEach(ngMock.module('kibana', function (PrivateProvider) { + stubRegistry = new Registry({ + order: ['order'] + }); + + PrivateProvider.swap(chromeNavControlsRegistry, stubRegistry); + })); + + beforeEach(ngMock.inject(function ($compile, $rootScope) { + compile = function () { + const $el = $('
    '); + $rootScope.$apply(); + $compile($el)($rootScope); + return $el; + }; + })); + + it('injects templates from the ui/registry/chrome_nav_controls registry', function () { + stubRegistry.register(function () { + return { + name: 'control', + order: 100, + template: `` + }; + }); + + let $el = compile(); + expect($el.find('#testTemplateEl')).to.have.length(1); + }); + + it('renders controls in reverse order, assuming that each control will float:right', function () { + stubRegistry.register(function () { + return { + name: 'control2', + order: 2, + template: `` + }; + }); + stubRegistry.register(function () { + return { + name: 'control1', + order: 1, + template: `` + }; + }); + stubRegistry.register(function () { + return { + name: 'control3', + order: 3, + template: `` + }; + }); + + let $el = compile(); + expect( + $el.find('.testControl') + .toArray() + .map(el => el.id) + ).to.eql(['3', '2', '1']); + }); +}); diff --git a/src/ui/public/chrome/__tests__/tab.js b/src/ui/public/chrome/__tests__/tab.js new file mode 100644 index 0000000000000..cc3d3ab1b4ba2 --- /dev/null +++ b/src/ui/public/chrome/__tests__/tab.js @@ -0,0 +1,254 @@ +import sinon from 'auto-release-sinon'; + +import Tab from '../tab'; +import expect from 'expect.js'; +import StubBrowserStorage from './fixtures/stub_browser_storage'; + +describe('Chrome Tab', function () { + describe('construction', function () { + it('accepts id, title, resetWhenActive, trackLastUrl, activeIndicatorColor, baseUrl', function () { + const tab = new Tab({ + id: 'foo', + title: 'Foo App', + resetWhenActive: false, + activeIndicatorColor: true, + baseUrl: 'proto:host.domain:999' + }); + + expect(tab.id).to.equal('foo'); + expect(tab.title).to.equal('Foo App'); + expect(tab.resetWhenActive).to.equal(false); + expect(tab.activeIndicatorColor).to.equal(true); + expect(tab.rootUrl).to.equal('proto:host.domain:999/foo'); + + const tab2 = new Tab({ + id: 'bar', + title: 'Bar App', + resetWhenActive: true, + activeIndicatorColor: false, + baseUrl: 'proto:host.domain:999/sub/#/' + }); + + expect(tab2.id).to.equal('bar'); + expect(tab2.title).to.equal('Bar App'); + expect(tab2.resetWhenActive).to.equal(true); + expect(tab2.activeIndicatorColor).to.equal(null); + expect(tab2.rootUrl).to.equal('proto:host.domain:999/sub/#/bar'); + }); + + it('starts inactive', function () { + const tab = new Tab(); + expect(tab.active).to.equal(false); + }); + + it('uses the id to set the rootUrl', function () { + const id = 'foo'; + const tab = new Tab({ id }); + expect(tab.id).to.equal(id); + expect(tab.rootUrl).to.equal(`/${id}`); + }); + + it('creates a regexp for matching the rootUrl', function () { + const tab = new Tab({ id: 'foo' }); + + expect('/foo').to.match(tab.rootRegExp); + expect('/foo/bar').to.match(tab.rootRegExp); + expect('/foo/bar/max').to.match(tab.rootRegExp); + expect('/foo?bar=baz').to.match(tab.rootRegExp); + expect('/foo/?bar=baz').to.match(tab.rootRegExp); + expect('/foo#?bar=baz').to.match(tab.rootRegExp); + + expect('/foobar').to.not.match(tab.rootRegExp); + expect('site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); + expect('http://site.com/foo#?bar=baz').to.not.match(tab.rootRegExp); + }); + + it('includes the baseUrl in the rootRegExp if specified', function () { + const tab = new Tab({ + id: 'foo', + baseUrl: 'http://spiderman.com/kibana' + }); + + expect('http://spiderman.com/kibana/foo/bar').to.match(tab.rootRegExp); + + expect('/foo').to.not.match(tab.rootRegExp); + expect('https://spiderman.com/kibana/foo/bar').to.not.match(tab.rootRegExp); + }); + + it('accepts a function for activeIndicatorColor', function () { + let i = 0; + const tab = new Tab({ + activeIndicatorColor: function () { + return i++; + } + }); + expect(tab.activeIndicatorColor).to.equal(0); + expect(tab.activeIndicatorColor).to.equal(1); + expect(tab.activeIndicatorColor).to.equal(2); + expect(tab.activeIndicatorColor).to.equal(3); + }); + + it('discovers the lastUrl', function () { + const lastUrlStore = new StubBrowserStorage(); + const tab = new Tab({ id: 'foo', lastUrlStore }); + expect(tab.lastUrl).to.not.equal('/foo/bar'); + + tab.setLastUrl('/foo/bar'); + expect(tab.lastUrl).to.equal('/foo/bar'); + + const tab2 = new Tab({ id: 'foo', lastUrlStore }); + expect(tab2.lastUrl).to.equal('/foo/bar'); + }); + + it('logs a warning about last urls that do not match the rootUrl', function () { + const lastUrlStore = new StubBrowserStorage(); + const tab = new Tab({ id: 'foo', baseUrl: '/bar', lastUrlStore }); + tab.setLastUrl('/bar/foo/1'); + + const stub = sinon.stub(console, 'log'); + const tab2 = new Tab({ id: 'foo', baseUrl: '/baz', lastUrlStore }); + sinon.assert.calledOnce(stub); + expect(tab2.lastUrl).to.equal(null); + }); + }); + + + describe('#setLastUrl()', function () { + it('updates the lastUrl and storage value if passed a lastUrlStore', function () { + const lastUrlStore = new StubBrowserStorage(); + const tab = new Tab({ id: 'foo', lastUrlStore }); + + expect(tab.lastUrl).to.not.equal('foo'); + tab.setLastUrl('foo'); + expect(tab.lastUrl).to.equal('foo'); + expect(lastUrlStore.getItem(tab.lastUrlStoreKey)).to.equal('foo'); + }); + + it('only updates lastUrl if no lastUrlStore', function () { + const tab = new Tab({ id: 'foo' }); + + expect(tab.lastUrl).to.equal(null); + tab.setLastUrl('foo'); + expect(tab.lastUrl).to.equal('foo'); + + const tab2 = new Tab({ id: 'foo' }); + expect(tab2.lastUrl).to.not.equal('foo'); + }); + }); + + + describe('#href()', function () { + it('returns the rootUrl/id be default', function () { + const tab = new Tab({ id: 'foo' }); + expect(tab.href()).to.equal(tab.rootUrl); + }); + + it('returns the lastUrl if tracking is on', function () { + const tab = new Tab({ id: 'foo' }); + tab.setLastUrl('okay'); + expect(tab.href()).to.equal('okay'); + }); + + describe('when the tab is active', function () { + it('returns the rootUrl when resetWhenActive: true', function () { + const id = 'foo'; + const resetWhenActive = true; + const tab = new Tab({ id, resetWhenActive }); + + tab.active = true; + + expect(tab.href()).to.not.equal('butt'); + expect(tab.href()).to.equal(tab.rootUrl); + }); + + it('or returns null when not', function () { + const tab = new Tab({ id: 'foo', resetWhenActive: false }); + tab.active = true; + + expect(tab.href()).to.not.equal('butt'); + expect(tab.href()).to.equal(null); + }); + }); + }); + + describe('#getLastPath()', function () { + it('parses a path out of the lastUrl by removing the baseUrl', function () { + const baseUrl = 'http://local:5601/app/visualize#'; + const tab = new Tab({ baseUrl }); + + tab.setLastUrl('http://local:5601/app/visualize#/index'); + expect(tab.getLastPath()).to.equal('/index'); + }); + + it('logs a warning if the lastUrl does not extend the root url', function () { + const baseUrl = 'http://local:5601/app/visualize#'; + const tab = new Tab({ baseUrl }); + sinon.stub(console, 'log'); + + tab.setLastUrl('http://local:5601/'); + tab.getLastPath(); + sinon.assert.calledOnce(console.log);// eslint-disable-line no-console + }); + }); + + describe('updateLastUrlGlobalState', function () { + const bases = [ + 'http://local:5601', + '', + 'weird.domain/with/subpath?path#', + 'weird.domain/with/#hashpath', + ]; + + context('with new state sets _g properly', function () { + const paths = [ + [ '/', '/?_g=newState' ], + [ '/?first', '/?first=&_g=newState' ], + [ '/path?first=1&_g=afterHash', '/path?first=1&_g=newState' ], + [ '/?first=1&_g=second', '/?first=1&_g=newState' ], + [ '/?g=first', '/?g=first&_g=newState' ], + [ '/a?first=1&_g=second', '/a?first=1&_g=newState' ], + [ '/?first=1&_g=second', '/?first=1&_g=newState' ], + [ '/?first&g=second', '/?first=&g=second&_g=newState' ], + ]; + + bases.forEach(baseUrl => { + paths.forEach(([pathFrom, pathTo]) => { + const fromUrl = `${baseUrl}${pathFrom}`; + const toUrl = `${baseUrl}${pathTo}`; + it(`${fromUrl} => ${toUrl}`, function () { + const tab = new Tab({ baseUrl }); + tab.setLastUrl(fromUrl); + tab.updateLastUrlGlobalState('newState'); + expect(tab.getLastUrl()).to.equal(toUrl); + }); + }); + }); + }); + + context('with new empty state removes _g', function () { + const paths = [ + [ '/', '/' ], + [ '/?first', '/?first=' ], + [ '/path?first=1&_g=afterHash', '/path?first=1' ], + [ '/?first=1&_g=second', '/?first=1' ], + [ '/?g=first', '/?g=first' ], + [ '/a?first=1&_g=second', '/a?first=1' ], + [ '/?first=1&_g=second', '/?first=1' ], + [ '/?first&g=second', '/?first=&g=second' ], + ]; + + bases.forEach(baseUrl => { + paths.forEach(([pathFrom, pathTo]) => { + const fromUrl = `${baseUrl}${pathFrom}`; + const toUrl = `${baseUrl}${pathTo}`; + it(`${fromUrl}`, function () { + const tab = new Tab({ baseUrl }); + tab.setLastUrl(fromUrl); + tab.updateLastUrlGlobalState(); + expect(tab.getLastUrl()).to.equal(toUrl); + }); + }); + }); + }); + }); +}); diff --git a/src/ui/public/chrome/__tests__/tab_collection.js b/src/ui/public/chrome/__tests__/tab_collection.js new file mode 100644 index 0000000000000..1bfa9c20b8f34 --- /dev/null +++ b/src/ui/public/chrome/__tests__/tab_collection.js @@ -0,0 +1,73 @@ +import expect from 'expect.js'; + +import StubBrowserStorage from './fixtures/stub_browser_storage'; +import TabCollection from '../tab_collection'; +import Tab from '../tab'; +import { indexBy, random } from 'lodash'; + +describe('Chrome TabCollection', function () { + describe('empty state', function () { + it('has no tabs', function () { + const tabs = new TabCollection(); + expect(tabs.get()).to.eql([]); + }); + + it('has no active tab', function () { + const tabs = new TabCollection(); + expect(!tabs.getActive()).to.equal(true); + }); + }); + + describe('#set()', function () { + it('consumes an ordered list of Tab specs', function () { + const tabs = new TabCollection(); + tabs.set([ + { id: 'foo' }, + { id: 'bar' } + ]); + + const ts = tabs.get(); + expect(ts.length).to.equal(2); + expect(ts[0].id).to.equal('foo'); + expect(ts[1].id).to.equal('bar'); + }); + }); + + describe('#setDefaults()', function () { + it('applies the defaults used to create tabs', function () { + const tabs = new TabCollection(); + tabs.setDefaults({ id: 'thing' }); + tabs.set([ {} ]); + + expect(tabs.get()[0].id).to.equal('thing'); + }); + + it('recreates existing tabs with new defaults', function () { + const tabs = new TabCollection(); + tabs.set([ {} ]); + expect(!tabs.get()[0].id).to.equal(true); + + tabs.setDefaults({ id: 'thing' }); + expect(tabs.get()[0].id).to.equal('thing'); + }); + }); + + describe('#consumeRouteUpdate()', function () { + it('updates the active tab', function () { + const store = new StubBrowserStorage(); + const baseUrl = `http://localhost:${random(1000, 9999)}`; + const tabs = new TabCollection({ store, defaults: { baseUrl } }); + tabs.set([ + { id: 'a' }, + { id: 'b' } + ]); + + tabs.consumeRouteUpdate(`${baseUrl}/a`); + const {a, b} = indexBy(tabs.get(), 'id'); + expect(a.active).to.equal(true); + expect(b.active).to.equal(false); + expect(tabs.getActive()).to.equal(a); + }); + }); + +}); diff --git a/src/ui/public/chrome/api/__tests__/angular.js b/src/ui/public/chrome/api/__tests__/angular.js new file mode 100644 index 0000000000000..4f93d284a6924 --- /dev/null +++ b/src/ui/public/chrome/api/__tests__/angular.js @@ -0,0 +1,18 @@ +import expect from 'expect.js'; + +import kbnAngular from '../angular'; +import { noop } from 'lodash'; + +describe('Chrome API :: Angular', () => { + describe('location helper methods', () => { + it('should return the sub app based on the url', () => { + const chrome = { + getInjected: noop, + addBasePath: noop + }; + kbnAngular(chrome, {}); + }); + it('should return breadcrumbs based on the url', () => { + }); + }); +}); diff --git a/src/ui/public/chrome/api/__tests__/apps.js b/src/ui/public/chrome/api/__tests__/apps.js index ddb53ada7186e..273635b9cc418 100644 --- a/src/ui/public/chrome/api/__tests__/apps.js +++ b/src/ui/public/chrome/api/__tests__/apps.js @@ -1,7 +1,7 @@ -const expect = require('expect.js'); +import expect from 'expect.js'; -const setup = require('../apps'); -const TabFakeStore = require('../../__tests__/_TabFakeStore'); +import setup from '../apps'; +import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage'; describe('Chrome API :: apps', function () { describe('#get/setShowAppsLink()', function () { @@ -86,11 +86,11 @@ describe('Chrome API :: apps', function () { describe('#getAppUrl()', function () { it('returns the resolved url of the current app', function () { const chrome = {}; - const app = { url: '/foo' }; + const app = { navLink: { url: '/foo' } }; setup(chrome, { app }); const a = document.createElement('a'); - a.setAttribute('href', app.url); + a.setAttribute('href', app.navLink.url); expect(chrome.getAppUrl()).to.equal(a.href); }); @@ -147,7 +147,7 @@ describe('Chrome API :: apps', function () { describe('#get/setLastUrlFor()', function () { it('reads/writes last url from storage', function () { const chrome = {}; - const store = new TabFakeStore(); + const store = new StubBrowserStorage(); setup(chrome, { appUrlStore: store }); expect(chrome.getLastUrlFor('app')).to.equal(undefined); chrome.setLastUrlFor('app', 'url'); diff --git a/src/ui/public/chrome/api/__tests__/nav.js b/src/ui/public/chrome/api/__tests__/nav.js index e3c89deedd60b..8dffa7e4eebbe 100644 --- a/src/ui/public/chrome/api/__tests__/nav.js +++ b/src/ui/public/chrome/api/__tests__/nav.js @@ -1,40 +1,73 @@ import expect from 'expect.js'; import initChromeNavApi from 'ui/chrome/api/nav'; +import StubBrowserStorage from '../../__tests__/fixtures/stub_browser_storage'; const basePath = '/someBasePath'; -function getChrome(customInternals = { basePath }) { +function init(customInternals = { basePath }) { const chrome = {}; - initChromeNavApi(chrome, { + const internals = { nav: [], ...customInternals, - }); - return chrome; + }; + initChromeNavApi(chrome, internals); + return { chrome, internals }; } describe('chrome nav apis', function () { describe('#getBasePath()', function () { it('returns the basePath', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.getBasePath()).to.be(basePath); }); }); describe('#addBasePath()', function () { it('returns undefined when nothing is passed', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath()).to.be(undefined); }); it('prepends the base path when the input is a path', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath('/other/path')).to.be(`${basePath}/other/path`); }); it('ignores non-path urls', function () { - const chrome = getChrome(); + const { chrome } = init(); expect(chrome.addBasePath('http://github.com/elastic/kibana')).to.be('http://github.com/elastic/kibana'); }); + + it('includes the query string', function () { + const { chrome } = init(); + expect(chrome.addBasePath('/app/kibana?a=b')).to.be(`${basePath}/app/kibana?a=b`); + }); + }); + + describe('internals.trackPossibleSubUrl()', function () { + it('injects the globalState of the current url to all links for the same app', function () { + const appUrlStore = new StubBrowserStorage(); + const nav = [ + { url: 'https://localhost:9200/app/kibana#discover' }, + { url: 'https://localhost:9200/app/kibana#visualize' }, + { url: 'https://localhost:9200/app/kibana#dashboard' }, + ].map(l => { + l.lastSubUrl = l.url; + return l; + }); + + const { chrome, internals } = init({ appUrlStore, nav }); + + internals.trackPossibleSubUrl('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(internals.nav[0].lastSubUrl).to.be('https://localhost:9200/app/kibana#discover?_g=globalstate'); + expect(internals.nav[0].active).to.be(false); + + expect(internals.nav[1].lastSubUrl).to.be('https://localhost:9200/app/kibana#visualize?_g=globalstate'); + expect(internals.nav[1].active).to.be(false); + + expect(internals.nav[2].lastSubUrl).to.be('https://localhost:9200/app/kibana#dashboard?_g=globalstate'); + expect(internals.nav[2].active).to.be(true); + }); }); }); diff --git a/src/ui/public/chrome/api/__tests__/xsrf.js b/src/ui/public/chrome/api/__tests__/xsrf.js index 9603a0fe35f7a..8bd5d3b9cd3b0 100644 --- a/src/ui/public/chrome/api/__tests__/xsrf.js +++ b/src/ui/public/chrome/api/__tests__/xsrf.js @@ -1,47 +1,46 @@ import $ from 'jquery'; import expect from 'expect.js'; import { stub } from 'auto-release-sinon'; -import ngMock from 'ngMock'; +import ngMock from 'ng_mock'; import xsrfChromeApi from '../xsrf'; +import { version } from '../../../../../../package.json'; -const xsrfHeader = 'kbn-xsrf-token'; -const xsrfToken = 'xsrfToken'; +const xsrfHeader = 'kbn-version'; describe('chrome xsrf apis', function () { describe('#getXsrfToken()', function () { it('exposes the token', function () { const chrome = {}; - xsrfChromeApi(chrome, { xsrfToken }); - expect(chrome.getXsrfToken()).to.be(xsrfToken); + xsrfChromeApi(chrome, { version }); + expect(chrome.getXsrfToken()).to.be(version); }); }); context('jQuery support', function () { it('adds a global jQuery prefilter', function () { stub($, 'ajaxPrefilter'); - xsrfChromeApi({}, {}); + xsrfChromeApi({}, { version }); expect($.ajaxPrefilter.callCount).to.be(1); }); context('jQuery prefilter', function () { let prefilter; - const xsrfToken = 'xsrfToken'; beforeEach(function () { stub($, 'ajaxPrefilter'); - xsrfChromeApi({}, { xsrfToken }); + xsrfChromeApi({}, { version }); prefilter = $.ajaxPrefilter.args[0][0]; }); - it('sets the kbn-xsrf-token header', function () { + it(`sets the ${xsrfHeader} header`, function () { const setHeader = stub(); prefilter({}, {}, { setRequestHeader: setHeader }); expect(setHeader.callCount).to.be(1); expect(setHeader.args[0]).to.eql([ xsrfHeader, - xsrfToken + version ]); }); @@ -60,7 +59,7 @@ describe('chrome xsrf apis', function () { beforeEach(function () { stub($, 'ajaxPrefilter'); const chrome = {}; - xsrfChromeApi(chrome, { xsrfToken }); + xsrfChromeApi(chrome, { version }); ngMock.module(chrome.$setupXsrfRequestInterceptor); }); @@ -78,9 +77,9 @@ describe('chrome xsrf apis', function () { $httpBackend.verifyNoOutstandingRequest(); }); - it('injects a kbn-xsrf-token header on every request', function () { + it(`injects a ${xsrfHeader} header on every request`, function () { $httpBackend.expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === xsrfToken; + return headers[xsrfHeader] === version; }).respond(200, ''); $http.post('/api/test'); @@ -113,10 +112,10 @@ describe('chrome xsrf apis', function () { $httpBackend.flush(); }); - it('accepts alternate tokens to use', function () { - const customToken = `custom:${xsrfToken}`; + it('treats the kbnXsrfToken option as boolean-y', function () { + const customToken = `custom:${version}`; $httpBackend.expectPOST('/api/test', undefined, function (headers) { - return headers[xsrfHeader] === customToken; + return headers[xsrfHeader] === version; }).respond(200, ''); $http({ diff --git a/src/ui/public/chrome/api/angular.js b/src/ui/public/chrome/api/angular.js index fa8671968f600..fb9a21982dea5 100644 --- a/src/ui/public/chrome/api/angular.js +++ b/src/ui/public/chrome/api/angular.js @@ -1,14 +1,19 @@ -var $ = require('jquery'); -var _ = require('lodash'); +import _ from 'lodash'; +import { format as formatUrl, parse as parseUrl } from 'url'; -require('../appSwitcher'); -var modules = require('ui/modules'); -var ConfigTemplate = require('ui/ConfigTemplate'); -require('ui/directives/config'); +import modules from 'ui/modules'; +import Notifier from 'ui/notify/notifier'; +import { UrlOverflowServiceProvider } from '../../error_url_overflow'; + +const URL_LIMIT_WARN_WITHIN = 150; module.exports = function (chrome, internals) { + + chrome.getFirstPathSegment = _.noop; + chrome.getBreadcrumbs = _.noop; + chrome.setupAngular = function () { - var kibana = modules.get('kibana'); + let kibana = modules.get('kibana'); _.forOwn(chrome.getInjected(), function (val, name) { kibana.value(name, val); @@ -18,60 +23,51 @@ module.exports = function (chrome, internals) { .value('kbnVersion', internals.version) .value('buildNum', internals.buildNum) .value('buildSha', internals.buildSha) + .value('serverName', internals.serverName) + .value('uiSettings', internals.uiSettings) .value('sessionId', Date.now()) + .value('chrome', chrome) .value('esUrl', (function () { - var a = document.createElement('a'); + let a = document.createElement('a'); a.href = chrome.addBasePath('/elasticsearch'); return a.href; }())) .config(chrome.$setupXsrfRequestInterceptor) - .directive('kbnChrome', function ($rootScope) { - return { - template: function ($el) { - var $content = $(require('ui/chrome/chrome.html')); - var $app = $content.find('.application'); - - if (internals.rootController) { - $app.attr('ng-controller', internals.rootController); - } - - if (internals.rootTemplate) { - $app.removeAttr('ng-view'); - $app.html(internals.rootTemplate); - } - - return $content; - }, - controllerAs: 'chrome', - controller: function ($scope, $rootScope, $location, $http) { - - // are we showing the embedded version of the chrome? - internals.setVisibleDefault(!$location.search().embed); - - // listen for route changes, propogate to tabs - var onRouteChange = function () { - let { href } = window.location; - let persist = chrome.getVisible(); - internals.trackPossibleSubUrl(href); - internals.tabs.consumeRouteUpdate(href, persist); - }; + .run(($location, $rootScope, Private) => { + chrome.getFirstPathSegment = () => { + return $location.path().split('/')[1]; + }; - $rootScope.$on('$routeChangeSuccess', onRouteChange); - $rootScope.$on('$routeUpdate', onRouteChange); - onRouteChange(); + chrome.getBreadcrumbs = () => { + return $location.path().split('/').slice(1); + }; - // and some local values - $scope.httpActive = $http.pendingRequests; - $scope.notifList = require('ui/notify')._notifs; - $scope.appSwitcherTemplate = new ConfigTemplate({ - switcher: '' - }); + const notify = new Notifier(); + const urlOverflow = Private(UrlOverflowServiceProvider); + const check = (event) => { + if ($location.path() === '/error/url-overflow') return; - return chrome; + try { + if (urlOverflow.check($location.absUrl()) <= URL_LIMIT_WARN_WITHIN) { + notify.warning(` + The URL has gotten big and may cause Kibana + to stop working. Please simplify the data on screen. + `); + } + } catch (e) { + const { host, path, search, protocol } = parseUrl(window.location.href); + // rewrite the entire url to force the browser to reload and + // discard any potentially unstable state from before + window.location.href = formatUrl({ host, path, search, protocol, hash: '#/error/url-overflow' }); } }; + + $rootScope.$on('$routeUpdate', check); + $rootScope.$on('$routeChangeStart', check); }); + require('../directives')(chrome, internals); + modules.link(kibana); }; diff --git a/src/ui/public/chrome/api/apps.js b/src/ui/public/chrome/api/apps.js index 35342d660f654..c94dd537c28c7 100644 --- a/src/ui/public/chrome/api/apps.js +++ b/src/ui/public/chrome/api/apps.js @@ -1,10 +1,10 @@ -const { clone, get } = require('lodash'); -const { resolve } = require('url'); +import { clone, get } from 'lodash'; +import { resolve } from 'url'; module.exports = function (chrome, internals) { - if (internals.app) { - internals.app.url = resolve(window.location.href, internals.app.url); + if (get(internals, 'app.navLink.url')) { + internals.app.navLink.url = resolve(window.location.href, internals.app.navLink.url); } internals.appUrlStore = internals.appUrlStore || window.sessionStorage; @@ -35,7 +35,7 @@ module.exports = function (chrome, internals) { }; chrome.getAppUrl = function () { - return get(internals, ['app', 'url']); + return get(internals, ['app', 'navLink', 'url']); }; chrome.getInjected = function (name, def) { diff --git a/src/ui/public/chrome/api/controls.js b/src/ui/public/chrome/api/controls.js index 116e37411cff5..93408b9c682d4 100644 --- a/src/ui/public/chrome/api/controls.js +++ b/src/ui/public/chrome/api/controls.js @@ -1,4 +1,4 @@ -var _ = require('lodash'); +import _ from 'lodash'; module.exports = function (chrome, internals) { /** @@ -10,7 +10,7 @@ module.exports = function (chrome, internals) { * determines if the Kibana chrome should be displayed */ - var def = true; + let def = true; internals.setVisibleDefault = (_def) => def = Boolean(_def); /** diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js index 3290010911513..73a2396510d3a 100644 --- a/src/ui/public/chrome/api/nav.js +++ b/src/ui/public/chrome/api/nav.js @@ -1,24 +1,20 @@ -const { startsWith, isString } = require('lodash'); import { parse, format } from 'url'; +import { startsWith, isString, find } from 'lodash'; export default function (chrome, internals) { chrome.getNavLinks = function () { return internals.nav; }; - chrome.getLastSubUrlFor = function (url) { - return internals.appUrlStore.getItem(`lastSubUrl:${url}`); - }; - chrome.getBasePath = function () { return internals.basePath || ''; }; chrome.addBasePath = function (url) { - var isUrl = url && isString(url); + let isUrl = url && isString(url); if (!isUrl) return url; - var parsed = parse(url); + let parsed = parse(url, true); if (!parsed.host && parsed.pathname) { if (parsed.pathname[0] === '/') { parsed.pathname = chrome.getBasePath() + parsed.pathname; @@ -34,26 +30,93 @@ export default function (chrome, internals) { }); }; + function lastSubUrlKey(link) { + return `lastSubUrl:${link.url}`; + } + + function setLastUrl(link, url) { + link.lastSubUrl = url; + internals.appUrlStore.setItem(lastSubUrlKey(link), url); + } + + function refreshLastUrl(link) { + link.lastSubUrl = internals.appUrlStore.getItem(lastSubUrlKey(link)) || link.lastSubUrl || link.url; + } + + function getAppId(url) { + const pathname = parse(url).pathname; + const pathnameWithoutBasepath = pathname.slice(chrome.getBasePath().length); + const match = pathnameWithoutBasepath.match(/^\/app\/([^\/]+)(?:\/|\?|#|$)/); + if (match) return match[1]; + } + + function decodeKibanaUrl(url) { + const parsedUrl = parse(url, true); + const appId = getAppId(parsedUrl); + const hash = parsedUrl.hash || ''; + const parsedHash = parse(hash.slice(1), true); + const globalState = parsedHash.query && parsedHash.query._g; + return { appId, globalState, parsedUrl, parsedHash }; + } + + function injectNewGlobalState(link, fromAppId, newGlobalState) { + // parse the lastSubUrl of this link so we can manipulate its parts + const { appId: toAppId, parsedHash: toHash, parsedUrl: toParsed } = decodeKibanaUrl(link.lastSubUrl); + + // don't copy global state if links are for different apps + if (fromAppId !== toAppId) return; + + // add the new globalState to the hashUrl in the linkurl + const toHashQuery = toHash.query || {}; + toHashQuery._g = newGlobalState; + + // format the new subUrl and include the newHash + link.lastSubUrl = format({ + protocol: toParsed.protocol, + port: toParsed.port, + hostname: toParsed.hostname, + pathname: toParsed.pathname, + query: toParsed.query, + hash: format({ + pathname: toHash.pathname, + query: toHashQuery, + hash: toHash.hash, + }), + }); + } + internals.trackPossibleSubUrl = function (url) { + const { appId, globalState: newGlobalState } = decodeKibanaUrl(url); + for (const link of internals.nav) { - if (startsWith(url, link.url)) { - link.lastSubUrl = url; - internals.appUrlStore.setItem(`lastSubUrl:${link.url}`, url); + const matchingTab = find(internals.tabs, { rootUrl: link.url }); + + link.active = startsWith(url, link.url); + if (link.active) { + setLastUrl(link, url); + continue; + } + + if (matchingTab) { + setLastUrl(link, matchingTab.getLastUrl()); + } else { + refreshLastUrl(link); + } + + if (newGlobalState) { + injectNewGlobalState(link, appId, newGlobalState); } } }; internals.nav.forEach(link => { // convert all link urls to absolute urls - - var a = document.createElement('a'); + let a = document.createElement('a'); a.setAttribute('href', link.url); link.url = a.href; - link.lastSubUrl = chrome.getLastSubUrlFor(link.url); - - if (link.url === chrome.getAppUrl()) { - link.active = true; - } }); + // simulate a possible change in url to initialize the + // link.active and link.lastUrl properties + internals.trackPossibleSubUrl(document.location.href); }; diff --git a/src/ui/public/chrome/api/tabs.js b/src/ui/public/chrome/api/tabs.js index 128f5ca8c267e..aba3734cfd1ce 100644 --- a/src/ui/public/chrome/api/tabs.js +++ b/src/ui/public/chrome/api/tabs.js @@ -1,5 +1,5 @@ -var _ = require('lodash'); -var TabCollection = require('../TabCollection'); +import _ from 'lodash'; +import TabCollection from '../tab_collection'; module.exports = function (chrome, internals) { @@ -76,12 +76,6 @@ module.exports = function (chrome, internals) { return internals.tabs.getActive(); }; - /** - * @param {any} def - the default value if there isn't any active tab - * @return {any} - */ - chrome.getActiveTabId = activeGetter('id'); - /** * @param {any} def - the default value if there isn't any active tab * @return {any} @@ -91,7 +85,7 @@ module.exports = function (chrome, internals) { // create a getter for properties of the active tab function activeGetter(prop) { return function (def) { - var active = chrome.getActiveTab(); + let active = chrome.getActiveTab(); return !active ? def : active[prop]; }; } diff --git a/src/ui/public/chrome/api/template.js b/src/ui/public/chrome/api/template.js index f45e3776caa88..2f8cc486fc2dc 100644 --- a/src/ui/public/chrome/api/template.js +++ b/src/ui/public/chrome/api/template.js @@ -1,4 +1,4 @@ -var _ = require('lodash'); +import _ from 'lodash'; module.exports = function (chrome, internals) { diff --git a/src/ui/public/chrome/api/theme.js b/src/ui/public/chrome/api/theme.js index 59b2dea3b3c24..192239cd91488 100644 --- a/src/ui/public/chrome/api/theme.js +++ b/src/ui/public/chrome/api/theme.js @@ -1,4 +1,4 @@ -var _ = require('lodash'); +import _ from 'lodash'; module.exports = function (chrome, internals) { /** @@ -66,7 +66,7 @@ module.exports = function (chrome, internals) { * @return {chrome} */ chrome.addApplicationClass = function (val) { - var classes = internals.applicationClasses || []; + let classes = internals.applicationClasses || []; classes.push(val); classes = _.uniq(classes); @@ -81,8 +81,8 @@ module.exports = function (chrome, internals) { * @return {chrome} */ chrome.removeApplicationClass = function (val) { - var classesToRemove = [].concat(val || []); - var classes = internals.applicationClasses || []; + let classesToRemove = [].concat(val || []); + let classes = internals.applicationClasses || []; _.pull(classes, ...classesToRemove); internals.applicationClasses = classes; diff --git a/src/ui/public/chrome/api/xsrf.js b/src/ui/public/chrome/api/xsrf.js index 244f709a9eaaa..4d04f9954231d 100644 --- a/src/ui/public/chrome/api/xsrf.js +++ b/src/ui/public/chrome/api/xsrf.js @@ -4,12 +4,12 @@ import { set } from 'lodash'; export default function (chrome, internals) { chrome.getXsrfToken = function () { - return internals.xsrfToken; + return internals.version; }; - $.ajaxPrefilter(function ({ kbnXsrfToken = internals.xsrfToken }, originalOptions, jqXHR) { + $.ajaxPrefilter(function ({ kbnXsrfToken = true }, originalOptions, jqXHR) { if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken); + jqXHR.setRequestHeader('kbn-version', internals.version); } }); @@ -17,9 +17,9 @@ export default function (chrome, internals) { $httpProvider.interceptors.push(function () { return { request: function (opts) { - const { kbnXsrfToken = internals.xsrfToken } = opts; + const { kbnXsrfToken = true } = opts; if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken); + set(opts, ['headers', 'kbn-version'], internals.version); } return opts; } diff --git a/src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js b/src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js deleted file mode 100644 index 35c33c7ef1f45..0000000000000 --- a/src/ui/public/chrome/appSwitcher/__tests__/appSwitcher.js +++ /dev/null @@ -1,221 +0,0 @@ -var sinon = require('auto-release-sinon'); -var ngMock = require('ngMock'); -var $ = require('jquery'); -var expect = require('expect.js'); -var constant = require('lodash').constant; -var set = require('lodash').set; -var cloneDeep = require('lodash').cloneDeep; -var indexBy = require('lodash').indexBy; - -require('ui/chrome'); -require('ui/chrome/appSwitcher'); -var DomLocationProvider = require('ui/domLocation'); - -describe('appSwitcher directive', function () { - var env; - - beforeEach(ngMock.module('kibana')); - - function setup(href, links) { - return ngMock.inject(function ($window, $rootScope, $compile, Private) { - var domLocation = Private(DomLocationProvider); - - $rootScope.chrome = { - getNavLinks: constant(cloneDeep(links)), - }; - - env = { - $scope: $rootScope, - $el: $compile($(''))($rootScope), - currentHref: href, - location: domLocation - }; - - Object.defineProperties(domLocation, { - href: { - get: function () { return env.currentHref; }, - set: function (val) { return env.currentHref = val; }, - }, - reload: { - value: sinon.stub() - } - }); - - env.$scope.$digest(); - }); - } - - context('when one link is for the active app', function () { - var myLink = { - active: true, - title: 'myLink', - url: 'http://localhost:555/app/myApp', - lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl' - }; - - var notMyLink = { - active: false, - title: 'notMyLink', - url: 'http://localhost:555/app/notMyApp', - lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl' - }; - - beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink])); - - it('links to the inactive apps base url', function () { - var $myLink = env.$el.findTestSubject('appLink').eq(0); - expect($myLink.prop('href')).to.be(myLink.url); - expect($myLink.prop('href')).to.not.be(myLink.lastSubUrl); - }); - - it('links to the inactive apps last sub url', function () { - var $notMyLink = env.$el.findTestSubject('appLink').eq(1); - expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl); - expect($notMyLink.prop('href')).to.not.be(notMyLink.url); - }); - }); - - context('when none of the links are for the active app', function () { - var myLink = { - active: false, - title: 'myLink', - url: 'http://localhost:555/app/myApp', - lastSubUrl: 'http://localhost:555/app/myApp#/lastSubUrl' - }; - - var notMyLink = { - active: false, - title: 'notMyLink', - url: 'http://localhost:555/app/notMyApp', - lastSubUrl: 'http://localhost:555/app/notMyApp#/lastSubUrl' - }; - - beforeEach(setup('http://localhost:5555/app/myApp/', [myLink, notMyLink])); - - it('links to the lastSubUrl for each', function () { - var $links = env.$el.findTestSubject('appLink'); - var $myLink = $links.eq(0); - var $notMyLink = $links.eq(1); - - expect($myLink.prop('href')).to.be(myLink.lastSubUrl); - expect($myLink.prop('href')).to.not.be(myLink.url); - - expect($notMyLink.prop('href')).to.be(notMyLink.lastSubUrl); - expect($notMyLink.prop('href')).to.not.be(notMyLink.url); - }); - }); - - context('clicking a link with matching href but missing hash', function () { - var url = 'http://localhost:555/app/myApp?query=1'; - beforeEach(setup(url + '#/lastSubUrl', [ - { url: url } - ])); - - it('just prevents propogation (no reload)', function () { - var event = new $.Event('click'); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(false); - - var $link = env.$el.findTestSubject('appLink'); - expect($link.prop('href')).to.be(url); - $link.trigger(event); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(true); - }); - }); - - context('clicking a link that matches entire url', function () { - var url = 'http://localhost:555/app/myApp#/lastSubUrl'; - beforeEach(setup(url, [ - { url: url } - ])); - - it('calls window.location.reload and prevents propogation', function () { - var event = new $.Event('click'); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(false); - - var $link = env.$el.findTestSubject('appLink'); - expect($link.prop('href')).to.be(env.currentHref); - $link.trigger(event); - - expect(env.location.reload.callCount).to.be(1); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(true); - }); - }); - - context('clicking a link with matching href but changed hash', function () { - var rootUrl = 'http://localhost:555/app/myApp?query=1'; - var url = rootUrl + '#/lastSubUrl2'; - - beforeEach(setup(url + '#/lastSubUrl', [ - { url: url } - ])); - - it('calls window.location.reload and prevents propogation', function () { - var event = new $.Event('click'); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(false); - - var $link = env.$el.findTestSubject('appLink'); - expect($link.prop('href')).to.be(url); - $link.trigger(event); - - expect(env.location.reload.callCount).to.be(1); - expect(event.isDefaultPrevented()).to.be(false); - expect(event.isPropagationStopped()).to.be(true); - }); - }); - - context('clicking a link with matching host', function () { - beforeEach(setup('http://localhost:555/someOtherPath', [ - { - active: true, - url: 'http://localhost:555/app/myApp' - } - ])); - - it('allows click through', function () { - var event = new $.Event('click'); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isPropagationStopped()).to.be(false); - - env.$el.findTestSubject('appLink').trigger(event); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isPropagationStopped()).to.be(false); - }); - }); - - context('clicking a link with matching host and path', function () { - beforeEach(setup('http://localhost:555/app/myApp?someQuery=true', [ - { - active: true, - url: 'http://localhost:555/app/myApp?differentQuery=true' - } - ])); - - it('allows click through', function () { - var event = new $.Event('click'); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isPropagationStopped()).to.be(false); - - env.$el.findTestSubject('appLink').trigger(event); - - expect(env.location.reload.callCount).to.be(0); - expect(event.isPropagationStopped()).to.be(false); - }); - }); - -}); diff --git a/src/ui/public/chrome/appSwitcher/appSwitcher.html b/src/ui/public/chrome/appSwitcher/appSwitcher.html deleted file mode 100644 index 7613cb03142b4..0000000000000 --- a/src/ui/public/chrome/appSwitcher/appSwitcher.html +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/src/ui/public/chrome/appSwitcher/appSwitcher.js b/src/ui/public/chrome/appSwitcher/appSwitcher.js deleted file mode 100644 index 61d2d6311381c..0000000000000 --- a/src/ui/public/chrome/appSwitcher/appSwitcher.js +++ /dev/null @@ -1,51 +0,0 @@ -var parse = require('url').parse; -var bindKey = require('lodash').bindKey; - -require('../appSwitcher/appSwitcher.less'); -var DomLocationProvider = require('ui/domLocation'); - -require('ui/modules') -.get('kibana') -.directive('appSwitcher', function () { - return { - restrict: 'E', - template: require('./appSwitcher.html'), - controllerAs: 'switcher', - controller: function ($scope, Private) { - var domLocation = Private(DomLocationProvider); - - // since we render this in an isolate scope we can't "require: ^chrome", but - // rather than remove all helpfull checks we can just check here. - if (!$scope.chrome || !$scope.chrome.getNavLinks) { - throw new TypeError('appSwitcher directive requires the "chrome" config-object'); - } - - this.getNavLinks = bindKey($scope.chrome, 'getNavLinks'); - - // links don't cause full-navigation events in certain scenarios - // so we force them when needed - this.ensureNavigation = function (event, app) { - if (event.isDefaultPrevented() || event.altKey || event.metaKey || event.ctrlKey) { - return; - } - - var toParsed = parse(event.delegateTarget.href); - var fromParsed = parse(domLocation.href); - var sameProto = toParsed.protocol === fromParsed.protocol; - var sameHost = toParsed.host === fromParsed.host; - var samePath = toParsed.path === fromParsed.path; - - if (sameProto && sameHost && samePath) { - toParsed.hash && domLocation.reload(); - - // event.preventDefault() keeps the browser from seeing the new url as an update - // and even setting window.location does not mimic that behavior, so instead - // we use stopPropagation() to prevent angular from seeing the click and - // starting a digest cycle/attempting to handle it in the router. - event.stopPropagation(); - } - }; - - } - }; -}); diff --git a/src/ui/public/chrome/appSwitcher/appSwitcher.less b/src/ui/public/chrome/appSwitcher/appSwitcher.less deleted file mode 100644 index 95ac50baa310c..0000000000000 --- a/src/ui/public/chrome/appSwitcher/appSwitcher.less +++ /dev/null @@ -1,58 +0,0 @@ -@import (reference) "~ui/styles/variables"; - -@app-icon-size: 48px; -@app-icon-padding: 10px; - -.app-links { - text-align: justify; - - .app-link { - display: inline-block; - vertical-align: top; - text-align: left; - width: @app-icon-size + (@app-icon-padding * 2); - margin: 0px 10px; - padding: @app-icon-padding; - border-radius: @border-radius-base; - - .app-icon { - display: block; - height: @app-icon-size; - width: @app-icon-size; - background-position: center; - background-size: contain; - border-radius: @border-radius-base; - background-color: @gray-light; - width: 100%; - - &-missing { - text-align: center; - font-size: 2.7em; - font-weight: bold; - font-family: @font-family-sans-serif; - color: #fff; - } - } - - .app-title { - color: @text-color; - font-size: 0.9em; - width: 100%; - text-align: center; - margin-top: 3px; - } - - &:hover .app-title { - text-decoration: underline; - } - - &.active { - background: @gray-lighter; - .app-title { - text-decoration: underline; - } - } - - } - -} diff --git a/src/ui/public/chrome/chrome.html b/src/ui/public/chrome/chrome.html index 4105363a942e6..f0cf8395d6a83 100644 --- a/src/ui/public/chrome/chrome.html +++ b/src/ui/public/chrome/chrome.html @@ -1,113 +1,71 @@ -
    -