diff --git a/package.json b/package.json index c49c26137d685..bd040ddc458cc 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dashboarding" ], "private": false, - "version": "5.0.0-reskin-snapshot", + "version": "5.0.0-snapshot", "build": { "number": 8467, "sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9" @@ -81,10 +81,10 @@ "ansicolors": "0.3.2", "autoprefixer": "5.1.1", "autoprefixer-loader": "2.0.0", - "babel": "5.8.23", - "babel-core": "5.8.23", + "babel": "5.8.38", + "babel-core": "5.8.38", "babel-loader": "5.3.2", - "babel-runtime": "5.8.20", + "babel-runtime": "5.8.38", "bluebird": "2.9.34", "boom": "2.8.0", "bootstrap": "3.3.6", diff --git a/src/plugins/kibana/public/visualize/styles/main.less b/src/plugins/kibana/public/visualize/styles/main.less index b0b18de2a573d..cfbd3addd324b 100644 --- a/src/plugins/kibana/public/visualize/styles/main.less +++ b/src/plugins/kibana/public/visualize/styles/main.less @@ -13,6 +13,14 @@ padding: 0; display: flex; + div.wizard-small { + flex: 2; + } + + div.wizard-large { + flex: 3; + } + .wizard-column { flex: 1; display: flex; @@ -45,11 +53,6 @@ .list-group { margin-bottom: 0; - - .list-group-item { - border-radius: 0; - border: none; - } } .striped { diff --git a/src/plugins/kibana/public/visualize/wizard/step_2.html b/src/plugins/kibana/public/visualize/wizard/step_2.html index e8cf5964b655d..bc49fab790d95 100644 --- a/src/plugins/kibana/public/visualize/wizard/step_2.html +++ b/src/plugins/kibana/public/visualize/wizard/step_2.html @@ -1,20 +1,15 @@
-
-

From a New Search

- -
-
-
Index Patterns
-
- -
+
+

From a New Search, Select Index

+ +
-
+

Or, From a Saved Search

{ - expect(res.payload).not.to.contain('Invalid cookie header'); - done(); + 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(); + }); }); }); - it('returns an error if the cookie can\'t be parsed', function (done) { - const options = { - method: 'GET', - url: '/', - headers: { - cookie: 'a' + describe('url shortener', () => { + const shortenOptions = { + method: 'POST', + url: '/shorten', + payload: { + url: '/app/kibana#/visualize/create' } }; - kbnTestServer.makeRequest(kbnServer, options, (res) => { - expect(res.payload).to.contain('Invalid cookie header'); - done(); + + 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/kbn_server.js b/src/server/kbn_server.js index d42554026b21f..5eb51ff77a9eb 100644 --- a/src/server/kbn_server.js +++ b/src/server/kbn_server.js @@ -20,6 +20,9 @@ module.exports = class KbnServer { require('./logging'), require('./status'), + // writes pid file + require('./pid'), + // find plugins and set this.plugins require('./plugins/scan'), @@ -74,9 +77,8 @@ module.exports = class KbnServer { 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); + server.log(['listening', 'info'], `Server running at ${server.info.uri}`); return server; } diff --git a/src/server/plugins/plugin.js b/src/server/plugins/plugin.js index 18c7786c15720..dfccce43c8331 100644 --- a/src/server/plugins/plugin.js +++ b/src/server/plugins/plugin.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import toPath from 'lodash/internal/toPath'; import Joi from 'joi'; import Bluebird, { attempt, fromNode } from 'bluebird'; import { basename, resolve } from 'path'; @@ -37,6 +38,8 @@ const defaultConfigSchema = Joi.object({ * @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. @@ -57,6 +60,7 @@ module.exports = class Plugin { this.requiredIds = opts.require || []; this.version = opts.version || pkg.version; this.externalInit = opts.init || _.noop; + this.configPrefix = opts.configPrefix || this.id; this.getConfigSchema = opts.config || _.noop; this.init = _.once(this.init); this[extendInitFns] = []; @@ -86,18 +90,18 @@ module.exports = class Plugin { async readConfig() { let schema = await this.getConfigSchema(Joi); let { config } = this.kbnServer; - config.extendSchema(this.id, schema || defaultConfigSchema); + config.extendSchema(this.configPrefix, schema || defaultConfigSchema); - if (config.get([this.id, 'enabled'])) { + if (config.get([...toPath(this.configPrefix), 'enabled'])) { return true; } else { - config.removeSchema(this.id); + config.removeSchema(this.configPrefix); return false; } } async init() { - let { id, version, kbnServer } = this; + let { id, version, kbnServer, configPrefix } = this; let { config } = kbnServer; // setup the hapi register function and get on with it @@ -132,7 +136,7 @@ module.exports = class Plugin { await fromNode(cb => { kbnServer.server.register({ register: register, - options: config.has(id) ? config.get(id) : null + options: config.has(configPrefix) ? config.get(configPrefix) : null }, cb); }); diff --git a/src/ui/public/directives/__tests__/paginated_selectable_list.js b/src/ui/public/directives/__tests__/paginated_selectable_list.js new file mode 100644 index 0000000000000..3d74e9923c5dd --- /dev/null +++ b/src/ui/public/directives/__tests__/paginated_selectable_list.js @@ -0,0 +1,165 @@ +import angular from 'angular'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import _ from 'lodash'; + +var objectList = [ + { title: 'apple' }, + { title: 'orange' }, + { title: 'coconut' }, + { title: 'banana' }, + { title: 'grapes' } +]; + +var stringList = [ + 'apple', + 'orange', + 'coconut', + 'banana', + 'grapes' +]; + +var lists = [objectList, stringList, []]; + +var $scope; +var $element; +var $isolatedScope; + +lists.forEach(function (list) { + var isArrayOfObjects = list.every((item) => { + return _.isPlainObject(item); + }); + + var init = function (arr) { + // Load the application + ngMock.module('kibana'); + + // Create the scope + ngMock.inject(function ($rootScope, $compile) { + $scope = $rootScope.$new(); + $scope.perPage = 5; + $scope.list = list; + $scope.listProperty = isArrayOfObjects ? 'title' : undefined; + $scope.test = function (val) { + return val; + }; + + // Create the element + $element = angular.element(''); + + // And compile it + $compile($element)($scope); + + // Fire a digest cycle + $element.scope().$digest(); + + // Grab the isolate scope so we can test it + $isolatedScope = $element.isolateScope(); + }); + }; + + describe('paginatedSelectableList', function () { + + describe('$scope.hits', function () { + beforeEach(function () { + init(list); + }); + + it('should initially sort an array of objects in ascending order', function () { + var property = $isolatedScope.listProperty; + var sortedList = property ? _.sortBy(list, property) : _.sortBy(list); + + expect($isolatedScope.hits).to.be.an('array'); + + $isolatedScope.hits.forEach(function (hit, index) { + if (property) { + expect(hit[property]).to.equal(sortedList[index][property]); + } else { + expect(hit).to.equal(sortedList[index]); + } + }); + }); + }); + + describe('$scope.sortHits', function () { + beforeEach(function () { + init(list); + }); + + it('should sort an array of objects in ascending order', function () { + var property = $isolatedScope.listProperty; + var sortedList = property ? _.sortBy(list, property) : _.sortBy(list); + + $isolatedScope.isAscending = false; + $isolatedScope.sortHits(list); + + expect($isolatedScope.isAscending).to.be(true); + + $isolatedScope.hits.forEach(function (hit, index) { + if (property) { + expect(hit[property]).to.equal(sortedList[index][property]); + } else { + expect(hit).to.equal(sortedList[index]); + } + }); + }); + + it('should sort an array of objects in descending order', function () { + var property = $isolatedScope.listProperty; + var reversedList = property ? _.sortBy(list, property).reverse() : _.sortBy(list).reverse(); + + $isolatedScope.isAscending = true; + $isolatedScope.sortHits(list); + + expect($isolatedScope.isAscending).to.be(false); + + $isolatedScope.hits.forEach(function (hit, index) { + if (property) { + expect(hit[property]).to.equal(reversedList[index][property]); + } else { + expect(hit).to.equal(reversedList[index]); + } + }); + }); + }); + + describe('$scope.makeUrl', function () { + beforeEach(function () { + init(list); + }); + + it('should return the result of the function its passed', function () { + var property = $isolatedScope.listProperty; + var sortedList = property ? _.sortBy(list, property) : _.sortBy(list); + + $isolatedScope.hits.forEach(function (hit, index) { + if (property) { + expect($isolatedScope.makeUrl(hit)[property]).to.equal(sortedList[index][property]); + } else { + expect($isolatedScope.makeUrl(hit)).to.equal(sortedList[index]); + } + }); + }); + }); + + describe('$scope.onSelect', function () { + beforeEach(function () { + init(list); + }); + + it('should return the result of the function its passed', function () { + var property = $isolatedScope.listProperty; + var sortedList = property ? _.sortBy(list, property) : _.sortBy(list); + + $isolatedScope.hits.forEach(function (hit, index) { + if (property) { + expect($isolatedScope.onSelect(hit)[property]).to.equal(sortedList[index][property]); + } else { + expect($isolatedScope.onSelect(hit)).to.equal(sortedList[index]); + } + }); + }); + }); + }); +}); diff --git a/src/ui/public/directives/paginate.js b/src/ui/public/directives/paginate.js index 08a9934753d38..2bcc0a57d075a 100644 --- a/src/ui/public/directives/paginate.js +++ b/src/ui/public/directives/paginate.js @@ -194,5 +194,3 @@ uiModules.get('kibana') template: paginateControlsTemplate }; }); - - diff --git a/src/ui/public/directives/paginated_selectable_list.js b/src/ui/public/directives/paginated_selectable_list.js new file mode 100644 index 0000000000000..7a63cca6fc9c5 --- /dev/null +++ b/src/ui/public/directives/paginated_selectable_list.js @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import uiModules from 'ui/modules'; +import paginatedSelectableListTemplate from 'ui/partials/paginated_selectable_list.html'; + +const module = uiModules.get('kibana'); + +module.directive('paginatedSelectableList', function (kbnUrl) { + + return { + restrict: 'E', + scope: { + perPage: '=?', + list: '=', + listProperty: '=', + userMakeUrl: '=?', + userOnSelect: '=?' + }, + template: paginatedSelectableListTemplate, + controller: function ($scope, $element, $filter) { + $scope.perPage = $scope.perPage || 10; + $scope.hits = $scope.list = _.sortBy($scope.list, accessor); + $scope.hitCount = $scope.hits.length; + + /** + * Boolean that keeps track of whether hits are sorted ascending (true) + * or descending (false) + * * @type {Boolean} + */ + $scope.isAscending = true; + + /** + * Sorts saved object finder hits either ascending or descending + * @param {Array} hits Array of saved finder object hits + * @return {Array} Array sorted either ascending or descending + */ + $scope.sortHits = function (hits) { + const sortedList = _.sortBy(hits, accessor); + + $scope.isAscending = !$scope.isAscending; + $scope.hits = $scope.isAscending ? sortedList : sortedList.reverse(); + }; + + $scope.makeUrl = function (hit) { + if ($scope.userMakeUrl) { + return $scope.userMakeUrl(hit); + } + }; + + $scope.onSelect = function (hit, $event) { + if ($scope.userOnSelect) { + return $scope.userOnSelect(hit, $event); + } + }; + + function accessor(val) { + const prop = $scope.listProperty; + return prop ? val[prop] : val; + } + } + }; +}); diff --git a/src/ui/public/partials/paginated_selectable_list.html b/src/ui/public/partials/paginated_selectable_list.html new file mode 100644 index 0000000000000..8b9874bf4f322 --- /dev/null +++ b/src/ui/public/partials/paginated_selectable_list.html @@ -0,0 +1,43 @@ +
+
+
+
+ + + + +
+
+ {{ (hits | filter: query).length }} of {{ hitCount }} +
+
+
+
+ +
    +
  • + + Name + + +
  • +
  • + + {{ hit }} + +
    + {{ hit }} +
    +
  • +
  • +

    No matches found.

    +
  • +
+
diff --git a/src/ui/public/partials/saved_object_finder.html b/src/ui/public/partials/saved_object_finder.html index e5eda0c18cdf8..11990711f3501 100644 --- a/src/ui/public/partials/saved_object_finder.html +++ b/src/ui/public/partials/saved_object_finder.html @@ -8,7 +8,7 @@ {{finder.hitCount}} of {{finder.hitCount}}
diff --git a/src/ui/public/share/__tests__/url_shortener.js b/src/ui/public/share/__tests__/url_shortener.js new file mode 100644 index 0000000000000..fd21cdd965044 --- /dev/null +++ b/src/ui/public/share/__tests__/url_shortener.js @@ -0,0 +1,124 @@ +import _ from 'lodash'; +import sinon from 'sinon'; +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import chrome from 'ui/chrome'; +import LibUrlShortenerProvider from 'ui/share/lib/url_shortener'; + +describe('Url shortener', () => { + let $rootScope; + let $location; + let $http; + let urlShortener; + let $httpBackend; + const shareId = 'id123'; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (_$rootScope_, _$location_, _$httpBackend_, Private) { + $location = _$location_; + $rootScope = _$rootScope_; + $httpBackend = _$httpBackend_; + urlShortener = Private(LibUrlShortenerProvider); + })); + + describe('Shorten without base path', () => { + it('should shorten urls with a port', function (done) { + $httpBackend.when('POST', '/shorten').respond(function (type, route, data) { + expect(JSON.parse(data).url).to.be('/app/kibana#123'); + return [200, shareId]; + }); + urlShortener.shortenUrl('http://localhost:5601/app/kibana#123').then(function (url) { + expect(url).to.be(`http://localhost:5601/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + it('should shorten urls without a port', function (done) { + $httpBackend.when('POST', '/shorten').respond(function (type, route, data) { + expect(JSON.parse(data).url).to.be('/app/kibana#123'); + return [200, shareId]; + }); + urlShortener.shortenUrl('http://localhost/app/kibana#123').then(function (url) { + expect(url).to.be(`http://localhost/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + }); + + describe('Shorten with base path', () => { + const basePath = '/foo'; + + let getBasePath; + beforeEach(ngMock.inject((Private) => { + getBasePath = sinon.stub(chrome, 'getBasePath', () => basePath); + urlShortener = Private(LibUrlShortenerProvider); + })); + + it('should shorten urls with a port', (done) => { + $httpBackend.when('POST', `${basePath}/shorten`).respond((type, route, data) => { + expect(JSON.parse(data).url).to.be('/app/kibana#123'); + return [200, shareId]; + }); + urlShortener.shortenUrl(`http://localhost:5601${basePath}/app/kibana#123`).then((url) => { + expect(url).to.be(`http://localhost:5601${basePath}/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + it('should shorten urls without a port', (done) => { + $httpBackend.when('POST', `${basePath}/shorten`).respond((type, route, data) => { + expect(JSON.parse(data).url).to.be('/app/kibana#123'); + return [200, shareId]; + }); + urlShortener.shortenUrl(`http://localhost${basePath}/app/kibana#123`).then((url) => { + expect(url).to.be(`http://localhost${basePath}/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + it('should shorten urls with a query string', (done) => { + $httpBackend.when('POST', `${basePath}/shorten`).respond((type, route, data) => { + expect(JSON.parse(data).url).to.be('/app/kibana?foo#123'); + return [200, shareId]; + }); + urlShortener.shortenUrl(`http://localhost${basePath}/app/kibana?foo#123`).then((url) => { + expect(url).to.be(`http://localhost${basePath}/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + it('should shorten urls without a hash', (done) => { + $httpBackend.when('POST', `${basePath}/shorten`).respond((type, route, data) => { + expect(JSON.parse(data).url).to.be('/app/kibana'); + return [200, shareId]; + }); + urlShortener.shortenUrl(`http://localhost${basePath}/app/kibana`).then((url) => { + expect(url).to.be(`http://localhost${basePath}/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + it('should shorten urls with a query string in the hash', (done) => { + const relativeUrl = "/app/kibana#/discover?_g=(refreshInterval:(display:Off,pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(_source),index:%27logstash-*%27,interval:auto,query:(query_string:(analyze_wildcard:!t,query:%27*%27)),sort:!(%27@timestamp%27,desc))"; //eslint-disable-line max-len, quotes + $httpBackend.when('POST', `${basePath}/shorten`).respond((type, route, data) => { + expect(JSON.parse(data).url).to.be(relativeUrl); + return [200, shareId]; + }); + urlShortener.shortenUrl(`http://localhost${basePath}${relativeUrl}`).then((url) => { + expect(url).to.be(`http://localhost${basePath}/goto/${shareId}`); + done(); + }); + $httpBackend.flush(); + }); + + afterEach(() => { + getBasePath.restore(); + }); + }); +}); diff --git a/src/ui/public/share/lib/url_shortener.js b/src/ui/public/share/lib/url_shortener.js index d20afb2a9af9e..76b755771fc15 100644 --- a/src/ui/public/share/lib/url_shortener.js +++ b/src/ui/public/share/lib/url_shortener.js @@ -1,24 +1,30 @@ import chrome from 'ui/chrome'; +import url from 'url'; export default function createUrlShortener(Notifier, $http, $location) { const notify = new Notifier({ location: 'Url Shortener' }); - const basePath = chrome.getBasePath(); - const baseUrl = `${$location.protocol()}://${$location.host()}:${$location.port()}${basePath}`; - async function shortenUrl(url) { - const relativeUrl = url.replace(baseUrl, ''); - const formData = { url: relativeUrl }; + function shortenUrl(absoluteUrl) { + const basePath = chrome.getBasePath(); + + const parsedUrl = url.parse(absoluteUrl); + const path = parsedUrl.path.replace(basePath, ''); + const hash = parsedUrl.hash ? parsedUrl.hash : ''; + const relativeUrl = path + hash; - try { - const result = await $http.post(`${basePath}/shorten`, formData); + const formData = { url: relativeUrl }; - return `${baseUrl}/goto/${result.data}`; - } catch (err) { - notify.error(err); - throw err; - } + return $http.post(`${basePath}/shorten`, formData).then((result) => { + return url.format({ + protocol: parsedUrl.protocol, + host: parsedUrl.host, + pathname: `${basePath}/goto/${result.data}` + }); + }).catch((response) => { + notify.error(response); + }); } return { diff --git a/src/ui/public/styles/base.less b/src/ui/public/styles/base.less index 62899e437691e..4a97d0d8d407e 100644 --- a/src/ui/public/styles/base.less +++ b/src/ui/public/styles/base.less @@ -320,7 +320,8 @@ bread-crumbs { } //== SavedObjectFinder -saved-object-finder { +saved-object-finder, +paginated-selectable-list { .row { background-color: @kibanaGray6; padding: 10px; @@ -328,6 +329,27 @@ saved-object-finder { flex-direction: row; } + .finder-hit-count, + .finder-manage-object { + min-width: 80px; + padding: 5px; + text-align: center; + } + + .finder-hit-count { + flex: 1; + + span { + color: @kibanaGray3; + } + } + + .finder-manage-object { + flex: 3; + text-align: left; + text-transform: capitalize; + } + .form-group { margin-bottom: 0; float: left; @@ -337,7 +359,6 @@ saved-object-finder { border: none; padding: 5px 0px; border-radius: @border-radius-base; - text-transform: capitalize; } span { @@ -350,30 +371,6 @@ saved-object-finder { width: 15px; } } - - .finder-hit-count, .finder-manage-object { - min-width: 80px; - padding: 5px; - } - - .finder-hit-count { - flex: 1; - text-align: center; - - span { - color: @kibanaGray3; - } - } - - .finder-manage-object { - flex: 3; - text-align: left; - text-transform: capitalize; - } - } - - .list-group-item-menu:hover { - background-color: transparent; } ul.li-striped { @@ -413,6 +410,7 @@ saved-object-finder { margin-right: 10px; } + display: block; color: @saved-object-finder-link-color !important; } @@ -464,6 +462,12 @@ saved-object-finder { } } } + + paginate { + paginate-controls { + margin: 20px; + } + } } // when rendered within a config dropdown, don't use a bottom margin diff --git a/src/ui/public/styles/dark-variables.less b/src/ui/public/styles/dark-variables.less index 3b07e8f8bf7b0..e61a7c7a8898e 100644 --- a/src/ui/public/styles/dark-variables.less +++ b/src/ui/public/styles/dark-variables.less @@ -156,4 +156,3 @@ @sidebar-bg: @btn-default-bg; @sidebar-hover-bg: darken(@btn-default-bg, 5%); @sidebar-hover-color: @text-color; - diff --git a/src/ui/public/styles/variables/for-theme.less b/src/ui/public/styles/variables/for-theme.less index 6aab3d69c557b..ab9933ecdbc4e 100644 --- a/src/ui/public/styles/variables/for-theme.less +++ b/src/ui/public/styles/variables/for-theme.less @@ -172,7 +172,7 @@ @list-group-menu-item-color: @link-color; @list-group-menu-item-select-color: @link-color; @list-group-menu-item-active-bg: @well-bg; -@list-group-menu-item-hover-bg: @well-bg; +@list-group-menu-item-hover-bg: @kibanaGray5; // Hint Box ====================================================================