Merge branch 'master' into testVisualize

This commit is contained in:
LeeDr 2015-12-22 15:28:07 -06:00
commit 6a0966ab0a
91 changed files with 2083 additions and 1143 deletions

View file

@ -825,7 +825,7 @@ Angular modules are defined using a custom require module named `ui/modules`. It
var app = require('ui/modules').get('app/namespace');
```
`app` above is a reference to an Angular module, and can be used to define controllers, providers and anything else used in Angular.
`app` above is a reference to an Angular module, and can be used to define controllers, providers and anything else used in Angular. While you can use this module to create/get any module with ui/modules, we generally use the "kibana" module for everything.
### Private modules
@ -838,6 +838,8 @@ app.controller('myController', function($scope, otherDeps, Private) {
});
```
*Use `Private` modules for everything except directives, filters, and controllers.*
### Promises
A more robust version of Angular's `$q` service is available as `Promise`. It can be used in the same way as `$q`, but it comes packaged with several utility methods that provide many of the same useful utilities as Bluebird.

View file

@ -21,22 +21,6 @@ bin/kibana plugin -i elasticsearch/marvel/latest
Because the organization given is `elasticsearch`, the plugin management tool automatically downloads the
plugin from `download.elastic.co`.
[float]
=== Installing Plugins from Github
When the specified plugin is not found at `download.elastic.co`, the plugin management tool parses the element
as a Github user name, as in the following example:
[source,shell]
bin/kibana plugin --install github-user/sample-plugin
Installing sample-plugin
Attempting to extract from https://download.elastic.co/github-user/sample-plugin/sample-plugin-latest.tar.gz
Attempting to extract from https://github.com/github-user/sample-plugin/archive/master.tar.gz
Downloading <some number> bytes....................
Extraction complete
Optimizing and caching browser bundles...
Plugin installation complete
[float]
=== Installing Plugins from an Arbitrary URL

View file

@ -62,6 +62,7 @@
"url": "https://github.com/elastic/kibana.git"
},
"dependencies": {
"@bigfunger/decompress-zip": "^0.2.0-stripfix2",
"@spalger/angular-bootstrap": "0.12.1",
"@spalger/filesaver": "1.1.2",
"@spalger/leaflet-draw": "0.2.3",
@ -85,6 +86,7 @@
"bootstrap": "3.3.5",
"brace": "0.5.1",
"bunyan": "1.4.0",
"clipboard": "1.5.5",
"commander": "2.8.1",
"css-loader": "0.17.0",
"d3": "3.5.6",
@ -166,7 +168,7 @@
"karma-ie-launcher": "0.2.0",
"karma-mocha": "0.2.0",
"karma-safari-launcher": "0.1.1",
"libesvm": "1.0.7",
"libesvm": "3.2.0",
"license-checker": "3.1.0",
"load-grunt-config": "0.7.2",
"marked-text-renderer": "0.1.0",

View file

@ -1,10 +1,6 @@
var expect = require('expect.js');
var sinon = require('sinon');
var plugin = require('../plugin');
var installer = require('../plugin_installer');
var remover = require('../pluginRemover');
var settingParser = require('../settingParser');
const expect = require('expect.js');
const sinon = require('sinon');
const plugin = require('../plugin');
describe('kibana cli', function () {
@ -12,7 +8,7 @@ describe('kibana cli', function () {
describe('commander options', function () {
var program = {
let program = {
command: function () { return program; },
description: function () { return program; },
option: function () { return program; },
@ -38,9 +34,9 @@ describe('kibana cli', function () {
});
it('should define the command line options', function () {
var spy = sinon.spy(program, 'option');
const spy = sinon.spy(program, 'option');
var options = [
const options = [
/-i/,
/-r/,
/-s/,
@ -50,10 +46,10 @@ describe('kibana cli', function () {
plugin(program);
for (var i = 0; i < spy.callCount; i++) {
var call = spy.getCall(i);
for (var o = 0; o < options.length; o++) {
var option = options[o];
for (let i = 0; i < spy.callCount; i++) {
const call = spy.getCall(i);
for (let o = 0; o < options.length; o++) {
const option = options[o];
if (call.args[0].match(option)) {
options.splice(o, 1);
break;

View file

@ -1,249 +0,0 @@
var expect = require('expect.js');
var sinon = require('sinon');
var nock = require('nock');
var glob = require('glob');
var rimraf = require('rimraf');
var { join } = require('path');
var pluginLogger = require('../pluginLogger');
var pluginDownloader = require('../pluginDownloader');
describe('kibana cli', function () {
describe('plugin downloader', function () {
var testWorkingPath = join(__dirname, '.test.data');
var logger;
var downloader;
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
describe('_downloadSingle', function () {
beforeEach(function () {
downloader = pluginDownloader({}, logger);
});
afterEach(function () {
});
it.skip('should throw an ENOTFOUND error for a 404 error', function () {
var couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.reply(404);
var source = 'http://www.files.com/plugin.tar.gz';
var errorStub = sinon.stub();
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/ENOTFOUND/);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
it.skip('should download and extract a valid plugin', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/plugin.tar.gz')
.replyWithFile(200, filename);
var source = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.then(function (data) {
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('should abort the download and extraction for a corrupt archive.', function () {
var filename = join(__dirname, 'replies/corrupt.tar.gz');
var couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.replyWithFile(200, filename);
var source = 'http://www.files.com/plugin.tar.gz';
var errorStub = sinon.stub();
return downloader._downloadSingle(source, testWorkingPath, 0, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
});
describe('download', function () {
beforeEach(function () {});
afterEach(function () {});
it.skip('should loop through bad urls until it finds a good one.', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'I am a bad uri',
'http://www.files.com/goodfile.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filename);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(0).args[0]).to.match(/badfile1.tar.gz/);
expect(logger.log.getCall(1).args[0]).to.match(/badfile2.tar.gz/);
expect(logger.log.getCall(2).args[0]).to.match(/I am a bad uri/);
expect(logger.log.getCall(3).args[0]).to.match(/goodfile.tar.gz/);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it.skip('should stop looping through urls when it finds a good one.', function () {
var filename = join(__dirname, 'replies/test-plugin-master.tar.gz');
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/goodfile.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filename)
.get('/badfile3.tar.gz')
.reply(404);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
for (var i = 0; i < logger.log.callCount; i++) {
expect(logger.log.getCall(i).args[0]).to.not.match(/badfile3.tar.gz/);
}
var files = glob.sync('**/*', { cwd: testWorkingPath });
var expected = [
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it.skip('should throw an error when it doesn\'t find a good url.', function () {
var settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
var couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/badfile3.tar.gz')
.reply(404);
var errorStub = sinon.stub();
return downloader.download(settings, logger)
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/not a valid/i);
var files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
});
});
});
});
});

View file

@ -1,28 +1,26 @@
var expect = require('expect.js');
var sinon = require('sinon');
var fs = require('fs');
var rimraf = require('rimraf');
const expect = require('expect.js');
const sinon = require('sinon');
const fs = require('fs');
const rimraf = require('rimraf');
var pluginCleaner = require('../plugin_cleaner');
var pluginLogger = require('../pluginLogger');
const pluginCleaner = require('../plugin_cleaner');
const pluginLogger = require('../plugin_logger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('pluginCleaner', function () {
var settings = {
const settings = {
workingPath: 'dummy'
};
describe('cleanPrevious', function () {
var cleaner;
var errorStub;
var logger;
var progress;
var request;
let cleaner;
let errorStub;
let logger;
let progress;
let request;
beforeEach(function () {
errorStub = sinon.stub();
@ -46,7 +44,7 @@ describe('kibana cli', function () {
it('should resolve if the working path does not exist', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync', function () {
var error = new Error('ENOENT');
const error = new Error('ENOENT');
error.code = 'ENOENT';
throw error;
});
@ -61,7 +59,7 @@ describe('kibana cli', function () {
it('should rethrow any exception except ENOENT from fs.statSync', function () {
sinon.stub(rimraf, 'sync');
sinon.stub(fs, 'statSync', function () {
var error = new Error('An Unhandled Error');
const error = new Error('An Unhandled Error');
throw error;
});
@ -112,8 +110,9 @@ describe('kibana cli', function () {
});
describe('cleanError', function () {
var cleaner;
var logger;
let cleaner;
let logger;
beforeEach(function () {
logger = pluginLogger(false);
cleaner = pluginCleaner(settings, logger);

View file

@ -0,0 +1,316 @@
const expect = require('expect.js');
const sinon = require('sinon');
const nock = require('nock');
const glob = require('glob');
const rimraf = require('rimraf');
const { join } = require('path');
const mkdirp = require('mkdirp');
const pluginLogger = require('../plugin_logger');
const pluginDownloader = require('../plugin_downloader');
describe('kibana cli', function () {
describe('plugin downloader', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
let logger;
let downloader;
function expectWorkingPathEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
expect(files).to.eql([]);
}
function expectWorkingPathNotEmpty() {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part'
];
expect(files.sort()).to.eql(expected.sort());
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
describe('_downloadSingle', function () {
beforeEach(function () {
const settings = {
urls: [],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
});
describe('http downloader', function () {
it('should download an unsupported file type, but return undefined for archiveType', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'image/jpeg'
})
.get('/banana.jpg')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/banana.jpg';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be(undefined);
expectWorkingPathNotEmpty();
});
});
it('should throw an ENOTFOUND error for a http ulr that returns 404', function () {
const couchdb = nock('http://www.files.com')
.get('/plugin.tar.gz')
.reply(404);
const sourceUrl = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should throw an ENOTFOUND error for an invalid url', function () {
const sourceUrl = 'i am an invalid url';
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should download a tarball from a valid http url', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'application/x-gzip'
})
.get('/plugin.tar.gz')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.tar.gz';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should download a zip from a valid http url', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.zip');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '341965',
'content-type': 'application/zip'
})
.get('/plugin.zip')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.zip';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.zip');
expectWorkingPathNotEmpty();
});
});
});
describe('local file downloader', function () {
it('should copy an unsupported file type, but return undefined for archiveType', function () {
const filePath = join(__dirname, 'replies/banana.jpg');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10',
'content-type': 'image/jpeg'
})
.get('/banana.jpg')
.replyWithFile(200, filePath);
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be(undefined);
expectWorkingPathNotEmpty();
});
});
it('should throw an ENOTFOUND error for an invalid local file', function () {
const filePath = join(__dirname, 'replies/i-am-not-there.tar.gz');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(shouldReject, function (err) {
expect(err.message).to.match(/ENOTFOUND/);
expectWorkingPathEmpty();
});
});
it('should copy a tarball from a valid local file', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should copy a zip from a valid local file', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.zip');
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.zip');
expectWorkingPathNotEmpty();
});
});
});
});
describe('download', function () {
it('should loop through bad urls until it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'I am a bad uri',
'http://www.files.com/goodfile.tar.gz'
],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filePath);
return downloader.download(settings, logger)
.then(function (data) {
expect(logger.log.getCall(0).args[0]).to.match(/badfile1.tar.gz/);
expect(logger.log.getCall(1).args[0]).to.match(/badfile2.tar.gz/);
expect(logger.log.getCall(2).args[0]).to.match(/I am a bad uri/);
expect(logger.log.getCall(3).args[0]).to.match(/goodfile.tar.gz/);
expectWorkingPathNotEmpty();
});
});
it('should stop looping through urls when it finds a good one.', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/goodfile.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/goodfile.tar.gz')
.replyWithFile(200, filePath)
.get('/badfile3.tar.gz')
.reply(404);
return downloader.download(settings, logger)
.then(function (data) {
for (let i = 0; i < logger.log.callCount; i++) {
expect(logger.log.getCall(i).args[0]).to.not.match(/badfile3.tar.gz/);
}
expectWorkingPathNotEmpty();
});
});
it('should throw an error when it doesn\'t find a good url.', function () {
const settings = {
urls: [
'http://www.files.com/badfile1.tar.gz',
'http://www.files.com/badfile2.tar.gz',
'http://www.files.com/badfile3.tar.gz'
],
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath,
timeout: 0
};
downloader = pluginDownloader(settings, logger);
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/badfile1.tar.gz')
.reply(404)
.get('/badfile2.tar.gz')
.reply(404)
.get('/badfile3.tar.gz')
.reply(404);
return downloader.download(settings, logger)
.then(shouldReject, function (err) {
expect(err.message).to.match(/no valid url specified/i);
expectWorkingPathEmpty();
});
});
});
});
});

View file

@ -0,0 +1,131 @@
const expect = require('expect.js');
const sinon = require('sinon');
const glob = require('glob');
const rimraf = require('rimraf');
const { join } = require('path');
const mkdirp = require('mkdirp');
const pluginLogger = require('../plugin_logger');
const extract = require('../plugin_extractor');
const pluginDownloader = require('../plugin_downloader');
describe('kibana cli', function () {
describe('plugin extractor', function () {
const testWorkingPath = join(__dirname, '.test.data');
const tempArchiveFilePath = join(testWorkingPath, 'archive.part');
let logger;
let downloader;
const settings = {
workingPath: testWorkingPath,
tempArchiveFile: tempArchiveFilePath
};
function shouldReject() {
throw new Error('expected the promise to reject');
}
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
rimraf.sync(testWorkingPath);
mkdirp.sync(testWorkingPath);
downloader = pluginDownloader(settings, logger);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
});
function copyReplyFile(filename) {
const filePath = join(__dirname, 'replies', filename);
const sourceUrl = 'file://' + filePath.replace(/\\/g, '/');
return downloader._downloadSingle(sourceUrl);
}
function shouldReject() {
throw new Error('expected the promise to reject');
}
describe('extractArchive', function () {
it('successfully extract a valid tarball', function () {
return copyReplyFile('test_plugin_master.tar.gz')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'README.md',
'index.js',
'package.json',
'public',
'public/app.js'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('successfully extract a valid zip', function () {
return copyReplyFile('test_plugin_master.zip')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(() => {
const files = glob.sync('**/*', { cwd: testWorkingPath });
const expected = [
'archive.part',
'README.md',
'index.js',
'package.json',
'public',
'public/app.js',
'extra file only in zip.txt'
];
expect(files.sort()).to.eql(expected.sort());
});
});
it('throw an error when extracting a corrupt zip', function () {
return copyReplyFile('corrupt.zip')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/error extracting/i);
});
});
it('throw an error when extracting a corrupt tarball', function () {
return copyReplyFile('corrupt.tar.gz')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/error extracting/i);
});
});
it('throw an error when passed an unknown archive type', function () {
return copyReplyFile('banana.jpg')
.then((data) => {
return extract(settings, logger, data.archiveType);
})
.then(shouldReject, (err) => {
expect(err.message).to.match(/unsupported archive format/i);
});
});
});
});
});

View file

@ -1,27 +1,22 @@
var expect = require('expect.js');
var sinon = require('sinon');
var nock = require('nock');
var rimraf = require('rimraf');
var fs = require('fs');
var { join } = require('path');
var Promise = require('bluebird');
var pluginLogger = require('../pluginLogger');
var pluginInstaller = require('../plugin_installer');
const expect = require('expect.js');
const sinon = require('sinon');
const rimraf = require('rimraf');
const { mkdirSync } = require('fs');
const { join } = require('path');
const pluginLogger = require('../plugin_logger');
const pluginInstaller = require('../plugin_installer');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('pluginInstaller', function () {
let logger;
let testWorkingPath;
let processExitStub;
var logger;
var testWorkingPath;
var processExitStub;
var statSyncStub;
beforeEach(function () {
processExitStub = undefined;
statSyncStub = undefined;
logger = pluginLogger(false);
testWorkingPath = join(__dirname, '.test.data');
rimraf.sync(testWorkingPath);
@ -31,7 +26,6 @@ describe('kibana cli', function () {
afterEach(function () {
if (processExitStub) processExitStub.restore();
if (statSyncStub) statSyncStub.restore();
logger.log.restore();
logger.error.restore();
rimraf.sync(testWorkingPath);
@ -39,9 +33,9 @@ describe('kibana cli', function () {
it('should throw an error if the workingPath already exists.', function () {
processExitStub = sinon.stub(process, 'exit');
fs.mkdirSync(testWorkingPath);
mkdirSync(testWorkingPath);
var settings = {
let settings = {
pluginPath: testWorkingPath
};
@ -54,18 +48,6 @@ describe('kibana cli', function () {
});
});
it('should rethrow any non "ENOENT" error from fs.', function () {
statSyncStub = sinon.stub(fs, 'statSync', function () {
throw new Error('This is unexpected.');
});
var settings = {
pluginPath: testWorkingPath
};
expect(pluginInstaller.install).withArgs(settings, logger).to.throwException(/this is unexpected/i);
});
});
});

View file

@ -1,15 +1,13 @@
var expect = require('expect.js');
var sinon = require('sinon');
var pluginLogger = require('../pluginLogger');
const expect = require('expect.js');
const sinon = require('sinon');
const pluginLogger = require('../plugin_logger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('logger', function () {
var logger;
let logger;
describe('logger.log', function () {
@ -23,18 +21,18 @@ describe('kibana cli', function () {
it('should log messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
var message = 'this is my message';
const message = 'this is my message';
logger.log(message);
var callCount = process.stdout.write.callCount;
const callCount = process.stdout.write.callCount;
expect(process.stdout.write.getCall(callCount - 2).args[0]).to.be(message);
expect(process.stdout.write.getCall(callCount - 1).args[0]).to.be('\n');
});
it('should log messages to the console and append not append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
for (var i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
@ -58,10 +56,10 @@ describe('kibana cli', function () {
it('should not log any messages when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
var message = 'this is my message';
const message = 'this is my message';
logger.log(message);
for (var i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
@ -72,10 +70,10 @@ describe('kibana cli', function () {
it('should not log any messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
var message = 'this is my message';
const message = 'this is my message';
logger.log(message);
for (var i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
logger.log('.', true);
}
logger.log('Done!');
@ -97,7 +95,7 @@ describe('kibana cli', function () {
it('should log error messages to the console and append a new line', function () {
logger = pluginLogger({ silent: false, quiet: false });
var message = 'this is my error';
const message = 'this is my error';
logger.error(message);
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
@ -105,7 +103,7 @@ describe('kibana cli', function () {
it('should log error messages to the console when quiet is set', function () {
logger = pluginLogger({ silent: false, quiet: true });
var message = 'this is my error';
const message = 'this is my error';
logger.error(message);
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
@ -113,7 +111,7 @@ describe('kibana cli', function () {
it('should not log any error messages when silent is set', function () {
logger = pluginLogger({ silent: true, quiet: false });
var message = 'this is my error';
const message = 'this is my error';
logger.error(message);
expect(process.stderr.write.callCount).to.be(0);

View file

@ -1,301 +0,0 @@
var expect = require('expect.js');
var sinon = require('sinon');
var progressReporter = require('../progressReporter');
var pluginLogger = require('../pluginLogger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('progressReporter', function () {
var logger;
var progress;
var request;
beforeEach(function () {
logger = pluginLogger(false);
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
request = {
abort: sinon.stub(),
emit: sinon.stub()
};
progress = progressReporter(logger, request);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
});
describe('handleResponse', function () {
describe('bad response codes', function () {
function testErrorResponse(element, index, array) {
it('should set the state to error for response code = ' + element, function () {
progress.handleResponse({ statusCode: element });
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(errorStub.lastCall.args[0].message).to.match(/ENOTFOUND/);
});
});
}
var badCodes = [
'400', '401', '402', '403', '404', '405', '406', '407', '408', '409', '410',
'411', '412', '413', '414', '415', '416', '417', '500', '501', '502', '503',
'504', '505'
];
badCodes.forEach(testErrorResponse);
});
describe('good response codes', function () {
function testSuccessResponse(statusCode, index, array) {
it('should set the state to success for response code = ' + statusCode, function () {
progress.handleResponse({ statusCode: statusCode, headers: { 'content-length': 1000 } });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(logger.log.callCount - 2).args[0]).to.match(/1000/);
});
});
}
function testUnknownNumber(statusCode, index, array) {
it('should log "unknown number of" for response code = ' + statusCode + ' without content-length header', function () {
progress.handleResponse({ statusCode: statusCode, headers: {} });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(logger.log.callCount - 2).args[0]).to.match(/unknown number/);
});
});
}
var goodCodes = [
'200', '201', '202', '203', '204', '205', '206', '300', '301', '302', '303',
'304', '305', '306', '307'
];
goodCodes.forEach(testSuccessResponse);
goodCodes.forEach(testUnknownNumber);
});
});
describe('handleData', function () {
it('should do nothing if the reporter is in an error state', function () {
progress.handleResponse({ statusCode: 400 });
progress.handleData({ length: 100 });
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(progress.hasError()).to.be(true);
expect(request.abort.called).to.be(true);
expect(logger.log.callCount).to.be(0);
});
});
it('should do nothing if handleResponse hasn\'t successfully executed yet', function () {
progress.handleData({ length: 100 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(1);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
});
});
it('should do nothing if handleResponse was called without a content-length header', function () {
progress.handleResponse({ statusCode: 200, headers: {} });
progress.handleData({ length: 100 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(2);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(1).args[0]).to.match(/complete/i);
});
});
it('should show a max of 20 dots for full prgress', function () {
progress.handleResponse({ statusCode: 200, headers: { 'content-length': 1000 } });
progress.handleData({ length: 1000 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(logger.log.callCount).to.be(22);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(1).args[0]).to.be('.');
expect(logger.log.getCall(2).args[0]).to.be('.');
expect(logger.log.getCall(3).args[0]).to.be('.');
expect(logger.log.getCall(4).args[0]).to.be('.');
expect(logger.log.getCall(5).args[0]).to.be('.');
expect(logger.log.getCall(6).args[0]).to.be('.');
expect(logger.log.getCall(7).args[0]).to.be('.');
expect(logger.log.getCall(8).args[0]).to.be('.');
expect(logger.log.getCall(9).args[0]).to.be('.');
expect(logger.log.getCall(10).args[0]).to.be('.');
expect(logger.log.getCall(11).args[0]).to.be('.');
expect(logger.log.getCall(12).args[0]).to.be('.');
expect(logger.log.getCall(13).args[0]).to.be('.');
expect(logger.log.getCall(14).args[0]).to.be('.');
expect(logger.log.getCall(15).args[0]).to.be('.');
expect(logger.log.getCall(16).args[0]).to.be('.');
expect(logger.log.getCall(17).args[0]).to.be('.');
expect(logger.log.getCall(18).args[0]).to.be('.');
expect(logger.log.getCall(19).args[0]).to.be('.');
expect(logger.log.getCall(20).args[0]).to.be('.');
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
});
it('should show dot for each 5% of completion', function () {
progress.handleResponse({ statusCode: 200, headers: { 'content-length': 1000 } });
expect(logger.log.callCount).to.be(1);
progress.handleData({ length: 50 }); //5%
expect(logger.log.callCount).to.be(2);
progress.handleData({ length: 100 }); //15%
expect(logger.log.callCount).to.be(4);
progress.handleData({ length: 200 }); //25%
expect(logger.log.callCount).to.be(8);
progress.handleData({ length: 590 }); //94%
expect(logger.log.callCount).to.be(20);
progress.handleData({ length: 60 }); //100%
expect(logger.log.callCount).to.be(21);
//Any progress over 100% should be ignored.
progress.handleData({ length: 9999 });
expect(logger.log.callCount).to.be(21);
progress.handleEnd();
expect(logger.log.callCount).to.be(22);
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.getCall(0).args[0]).to.match(/downloading/i);
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
});
});
describe('handleEnd', function () {
it('should reject the deferred with a ENOTFOUND error if the reporter is in an error state', function () {
progress.handleResponse({ statusCode: 400 });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.firstCall.args[0].message).to.match(/ENOTFOUND/);
expect(errorStub.called).to.be(true);
});
});
it('should resolve if the reporter is not in an error state', function () {
progress.handleResponse({ statusCode: 307, headers: { 'content-length': 1000 } });
progress.handleEnd();
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(false);
expect(logger.log.lastCall.args[0]).to.match(/complete/i);
});
});
});
describe('handleError', function () {
it('should log any errors', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(logger.error.callCount).to.be(1);
expect(logger.error.lastCall.args[0]).to.match(/oops!/);
});
});
it('should set the error state of the reporter', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(progress.hasError()).to.be(true);
});
});
it('should ignore all errors except the first.', function () {
progress.handleError('ERRORMESSAGE', new Error('oops!'));
progress.handleError('ERRORMESSAGE', new Error('second error!'));
progress.handleError('ERRORMESSAGE', new Error('third error!'));
progress.handleError('ERRORMESSAGE', new Error('fourth error!'));
var errorStub = sinon.stub();
return progress.promise
.catch(errorStub)
.then(function (data) {
expect(errorStub.called).to.be(true);
expect(logger.error.callCount).to.be(1);
expect(logger.error.lastCall.args[0]).to.match(/oops!/);
});
});
});
});
});
});

View file

@ -0,0 +1,96 @@
const expect = require('expect.js');
const sinon = require('sinon');
const progressReporter = require('../progress_reporter');
const pluginLogger = require('../plugin_logger');
describe('kibana cli', function () {
describe('plugin installer', function () {
describe('progressReporter', function () {
let logger;
let progress;
let request;
beforeEach(function () {
logger = pluginLogger({ silent: false, quiet: false });
sinon.stub(logger, 'log');
sinon.stub(logger, 'error');
progress = progressReporter(logger);
});
afterEach(function () {
logger.log.restore();
logger.error.restore();
});
describe('handleData', function () {
it('should show a max of 20 dots for full progress', function () {
progress.init(1000);
progress.progress(1000);
progress.complete();
expect(logger.log.callCount).to.be(22);
expect(logger.log.getCall(0).args[0]).to.match(/transfer/i);
expect(logger.log.getCall(1).args[0]).to.be('.');
expect(logger.log.getCall(2).args[0]).to.be('.');
expect(logger.log.getCall(3).args[0]).to.be('.');
expect(logger.log.getCall(4).args[0]).to.be('.');
expect(logger.log.getCall(5).args[0]).to.be('.');
expect(logger.log.getCall(6).args[0]).to.be('.');
expect(logger.log.getCall(7).args[0]).to.be('.');
expect(logger.log.getCall(8).args[0]).to.be('.');
expect(logger.log.getCall(9).args[0]).to.be('.');
expect(logger.log.getCall(10).args[0]).to.be('.');
expect(logger.log.getCall(11).args[0]).to.be('.');
expect(logger.log.getCall(12).args[0]).to.be('.');
expect(logger.log.getCall(13).args[0]).to.be('.');
expect(logger.log.getCall(14).args[0]).to.be('.');
expect(logger.log.getCall(15).args[0]).to.be('.');
expect(logger.log.getCall(16).args[0]).to.be('.');
expect(logger.log.getCall(17).args[0]).to.be('.');
expect(logger.log.getCall(18).args[0]).to.be('.');
expect(logger.log.getCall(19).args[0]).to.be('.');
expect(logger.log.getCall(20).args[0]).to.be('.');
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
it('should show dot for each 5% of completion', function () {
progress.init(1000);
expect(logger.log.callCount).to.be(1);
progress.progress(50); //5%
expect(logger.log.callCount).to.be(2);
progress.progress(100); //15%
expect(logger.log.callCount).to.be(4);
progress.progress(200); //25%
expect(logger.log.callCount).to.be(8);
progress.progress(590); //94%
expect(logger.log.callCount).to.be(20);
progress.progress(60); //100%
expect(logger.log.callCount).to.be(21);
//Any progress over 100% should be ignored.
progress.progress(9999);
expect(logger.log.callCount).to.be(21);
progress.complete();
expect(logger.log.callCount).to.be(22);
expect(logger.log.getCall(0).args[0]).to.match(/transfer/i);
expect(logger.log.getCall(21).args[0]).to.match(/complete/i);
});
});
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View file

@ -0,0 +1,97 @@
504b 0304 1400 0000 0000 d575 7147 0000
0000 0000 0000 0000 0000 1300 0000 7465
7374 2d70 6c75 6769 6e2d 6d61 7374 6572
2f50 4b03 040a 0000 0000 00f2 63d8 46a5
06bf 880c 0000 000c 0000 001d 0000 0074
6573 742d 706c 7567 696e 2d6d 6173 7465
722f 2e67 6974 6967 6e6f 7265 6e6f 6465
5f6d 6f64 756c 6573 504b 0304 1400 0000
0800 f263 d846 38c6 e53d 9d00 0000 ee00
0000 1b00 0000 7465 7374 2d70 6c75 6769
6e2d 6d61 7374 6572 2f69 6e64 6578 2e6a
733d 8cc1 0e82 3010 44ef 7cc5 de80 4469
133d 413c f807 1efc 8182 ab36 96ed 06b6
d1c4 f0ef 16a8 cc61 9399 7d33 bdbf 0587
157e d80f 32c2 09ee 813a b19e a078 d9d6
9029 e19b 010c 2861 2020 7cc3 1a57 1717
1e96 8af8 8c4a f57a 6617 19e6 c524 8915
8735 e457 1c05 d626 9c99 f3dd 46d8 ce53
049e 225c 2bc5 ce74 d89a 9855 84a2 8e5a
ab83 d611 dff8 ded8 99e7 656b 5412 87f7
ab51 260e 276e cafe 772a 9b6c 6a7e 504b
0304 1400 0000 0800 f263 d846 5c85 06c2
0901 0000 dc01 0000 1f00 0000 7465 7374
2d70 6c75 6769 6e2d 6d61 7374 6572 2f70
6163 6b61 6765 2e6a 736f 6e5d 90cd 6ec3
2010 84ef 790a ea4b 5a29 218e dd46 6a6e
7d8f a812 c62b 1b97 0262 975a 5695 772f
60e7 a7e1 c67c bb33 03bf 2bc6 0a23 bea1
38b2 8200 69eb 74e8 9429 3609 fc80 4765
4d62 7b5e f272 565b 40e9 95a3 850c 0189
0996 96d9 fdb2 0767 5191 f553 9c4a 3951
a3c9 e5a4 4e51 1cca 52f0 3a29 3d91 3bee
7623 3471 0778 1a88 fc9c 9de6 38bc d944
6352 0649 e8bc 6b6c 0b6c 0b6c 2dad 41ab
816b db3d 9f8a 78eb bca0 a045 aa8a 1b36
d9c0 466b 9efe 9f53 f1b2 ce59 cbe3 1c98
168c 5470 17d8 e800 8df2 6d4a fbac f83b
afcb 4b7f d022 9691 7cc0 0cf7 bce2 8f0c
4178 d967 fcc6 cb1b 1eac cae2 81bf f2fa
226a db0a 9c87 eb18 74d5 470f f26b f138
448f 6b63 ad24 18cc dffa e184 ec61 5b25
7c5e fd01 504b 0304 1400 0000 0000 d575
7147 0000 0000 0000 0000 0000 0000 1a00
0000 7465 7374 2d70 6c75 6769 6e2d 6d61
7374 6572 2f70 7562 6c69 632f 504b 0304
1400 0000 0800 f263 d846 674a 6865 4a00
0000 4e00 0000 2000 0000 7465 7374 2d70
6c75 6769 6e2d 6d61 7374 6572 2f70 7562
6c69 632f 6170 702e 6a73 05c1 c10d 8020
1004 c0bf 55ac 2fa1 062b f169 6091 4bc8
a178 e7c7 d8bb 3399 4594 a1b8 2693 ae08
8397 cb60 c43b 017b e3b0 b06c dd51 f787
104d cd33 33ac 12c6 db70 363f 44e7 25ae
d317 d71f 504b 0304 0a00 0000 0000 f263
d846 ac2f 0f2b 1200 0000 1200 0000 1c00
0000 7465 7374 2d70 6c75 6769 6e2d 6d61
7374 6572 2f52 4541 444d 452e 6d64 4920
616d 2061 2074 6573 7420 706c 7567 696e
504b 0304 1400 0000 0000 4b7e 7147 0000
0000 0000 0000 0000 0000 2d00 0000 7465
7374 2d70 6c75 6769 6e2d 6d61 7374 6572
2f65 7874 7261 2066 696c 6520 6f6e 6c79
2069 6e20 7a69 702e 7478 7450 4b01 0214
0014 0000 0000 00d5 7571 4700 0000 0000
0000 0000 0000 0013 0024 0000 0000 0000
0010 0000 0000 0000 0074 6573 742d 706c
7567 696e 2d6d 6173 7465 722f 0a00 2000
0000 0000 0100 1800 4634 e20f 7921 d101
4634 e20f 7921 d101 d449 e10f 7921 d101
504b 0102 1400 0a00 0000 0000 f263 d846
a506 bf88 0c00 0000 0c00 0000 1d00 2400
0000 0000 0000 2000 0000 3100 0000 7465
7374 2d70 6c75 6769 6e2d 6d61 7374 6572
2f2e 6769 7469 676e 6f72 650a 0020 0000
0000 0001 0018 0000 f483 00ac aed0 0179
98e1 0f79 21d1 017
0000 0008 00f2 63d8 4667 4a68 654a 0000
004e 0000 0020 0024 0000 0000 0000 0020
0000 00cc 0200 0074 6573 742d 706c 7567
696e 2d6d 6173 7465 722f 7075 626c 6963
2f61 7070 2e6a 730a 0020 0000 0000 0001
0018 0000 f483 00ac aed0 015b 5be2 0f79
21d1 015b 5be2 0f79 21d1 0150 4b01 0214
000a 0000 0000 00f2 63d8 46ac 2f0f 2b12
0000 0012 0000 001c 0024 0000 0000 0000
0020 0000 0054 0300 0074 6573 742d 706c
7567 696e 2d6d 6173 7465 722f 5245 4144
4d45 2e6d 640a 0020 0000 0000 0001 0018
0000 f483 00ac aed0 014e 0de2 0f79 21d1
014e 0de2 0f79 21d1 0150 4b01 0214 0014
0000 0000 004b 7e71 4700 0000 0000 0000
0000 0000 002d 0000 0000 0000 0000 0020
0000 00a0 0300 0074 6573 742d 706c 7567
696e 2d6d 6173 7465 722f 6578 7472 6120
6669 6c65 206f 6e6c 7920 696e 207a 6970
2e74 7874 504b 0506 0000 0000 0800 0800
5903 0000 eb03 0000 0000

View file

@ -1,13 +0,0 @@
{
"name": "test-plugin",
"version": "1.0.0",
"description": "just a test plugin",
"repository": {
"type": "git",
"url": "http://website.git"
},
"dependencies": {
"bluebird": "2.9.30"
},
"license": "Apache-2.0"
}

View file

@ -3,7 +3,7 @@ var expect = require('expect.js');
var utils = require('requirefrom')('src/utils');
var fromRoot = utils('fromRoot');
var settingParser = require('../settingParser');
var settingParser = require('../setting_parser');
describe('kibana cli', function () {
@ -205,9 +205,8 @@ describe('kibana cli', function () {
var settings = parser.parse();
expect(settings.urls).to.have.property('length', 2);
expect(settings.urls).to.have.property('length', 1);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-latest.tar.gz');
expect(settings.urls).to.contain('https://github.com/kibana/test-plugin/archive/master.tar.gz');
});
it('should populate the urls collection properly version specified', function () {
@ -216,9 +215,8 @@ describe('kibana cli', function () {
var settings = parser.parse();
expect(settings.urls).to.have.property('length', 2);
expect(settings.urls).to.have.property('length', 1);
expect(settings.urls).to.contain('https://download.elastic.co/kibana/test-plugin/test-plugin-v1.1.1.tar.gz');
expect(settings.urls).to.contain('https://github.com/kibana/test-plugin/archive/v1.1.1.tar.gz');
});
it('should populate the pluginPath', function () {
@ -231,6 +229,26 @@ describe('kibana cli', function () {
expect(settings).to.have.property('pluginPath', expected);
});
it('should populate the workingPath', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = fromRoot('installedPlugins/.plugin.installing');
expect(settings).to.have.property('workingPath', expected);
});
it('should populate the tempArchiveFile', function () {
options.install = 'kibana/test-plugin';
parser = settingParser(options);
var settings = parser.parse();
var expected = fromRoot('installedPlugins/.plugin.installing/archive.part');
expect(settings).to.have.property('tempArchiveFile', expected);
});
describe('with url option', function () {
it('should allow one part to the install parameter', function () {

View file

@ -0,0 +1,76 @@
const { createWriteStream, createReadStream, unlinkSync, statSync } = require('fs');
const getProgressReporter = require('../progress_reporter');
function openSourceFile({ sourcePath }) {
try {
let fileInfo = statSync(sourcePath);
const readStream = createReadStream(sourcePath);
return { readStream, fileInfo };
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error('ENOTFOUND');
}
throw err;
}
}
async function copyFile({ readStream, writeStream, progressReporter }) {
await new Promise((resolve, reject) => {
// if either stream errors, fail quickly
readStream.on('error', reject);
writeStream.on('error', reject);
// report progress as we transfer
readStream.on('data', (chunk) => {
progressReporter.progress(chunk.length);
});
// write the download to the file system
readStream.pipe(writeStream);
// when the write is done, we are done
writeStream.on('finish', resolve);
});
}
function getArchiveTypeFromFilename(path) {
if (/\.zip$/i.test(path)) {
return '.zip';
}
if (/\.tar\.gz$/i.test(path)) {
return '.tar.gz';
}
}
/*
// Responsible for managing local file transfers
*/
export default async function copyLocalFile(logger, sourcePath, targetPath) {
try {
const { readStream, fileInfo } = openSourceFile({ sourcePath });
const writeStream = createWriteStream(targetPath);
try {
const progressReporter = getProgressReporter(logger);
progressReporter.init(fileInfo.size);
await copyFile({ readStream, writeStream, progressReporter });
progressReporter.complete();
} catch (err) {
readStream.close();
writeStream.close();
throw err;
}
// all is well, return our archive type
const archiveType = getArchiveTypeFromFilename(sourcePath);
return { archiveType };
} catch (err) {
logger.error(err);
throw err;
}
};

View file

@ -0,0 +1,85 @@
const { fromNode: fn } = require('bluebird');
const { createWriteStream, unlinkSync } = require('fs');
const Wreck = require('wreck');
const getProgressReporter = require('../progress_reporter');
function sendRequest({ sourceUrl, timeout }) {
const maxRedirects = 11; //Because this one goes to 11.
return fn(cb => {
const req = Wreck.request('GET', sourceUrl, { timeout, redirects: maxRedirects }, (err, resp) => {
if (err) {
if (err.code === 'ECONNREFUSED') {
err = new Error('ENOTFOUND');
}
return cb(err);
}
if (resp.statusCode >= 400) {
return cb(new Error('ENOTFOUND'));
}
cb(null, { req, resp });
});
});
}
function downloadResponse({ resp, targetPath, progressReporter }) {
return new Promise((resolve, reject) => {
const writeStream = createWriteStream(targetPath);
// if either stream errors, fail quickly
resp.on('error', reject);
writeStream.on('error', reject);
// report progress as we download
resp.on('data', (chunk) => {
progressReporter.progress(chunk.length);
});
// write the download to the file system
resp.pipe(writeStream);
// when the write is done, we are done
writeStream.on('finish', resolve);
});
}
function getArchiveTypeFromResponse(resp) {
const contentType = (resp.headers['content-type'] || '');
switch (contentType.toLowerCase()) {
case 'application/zip': return '.zip';
case 'application/x-gzip': return '.tar.gz';
}
}
/*
Responsible for managing http transfers
*/
export default async function downloadUrl(logger, sourceUrl, targetPath, timeout) {
try {
const { req, resp } = await sendRequest({ sourceUrl, timeout });
try {
let totalSize = parseFloat(resp.headers['content-length']) || 0;
const progressReporter = getProgressReporter(logger);
progressReporter.init(totalSize);
await downloadResponse({ resp, targetPath, progressReporter });
progressReporter.complete();
} catch (err) {
req.abort();
throw err;
}
// all is well, return our archive type
const archiveType = getArchiveTypeFromResponse(resp);
return { archiveType };
} catch (err) {
if (err.message !== 'ENOTFOUND') {
logger.error(err);
}
throw err;
}
};

View file

@ -0,0 +1,34 @@
const zlib = require('zlib');
const fs = require('fs');
const tar = require('tar');
async function extractArchive(settings) {
await new Promise((resolve, reject) => {
const gunzip = zlib.createGunzip();
const tarExtract = new tar.Extract({ path: settings.workingPath, strip: 1 });
const readStream = fs.createReadStream(settings.tempArchiveFile);
readStream.on('error', reject);
gunzip.on('error', reject);
tarExtract.on('error', reject);
readStream
.pipe(gunzip)
.pipe(tarExtract);
tarExtract.on('finish', resolve);
});
}
export default async function extractTarball(settings, logger) {
try {
logger.log('Extracting plugin archive');
await extractArchive(settings);
logger.log('Extraction complete');
} catch (err) {
logger.error(err);
throw new Error('Error extracting plugin archive');
}
};

View file

@ -0,0 +1,32 @@
const DecompressZip = require('@bigfunger/decompress-zip');
async function extractArchive(settings) {
await new Promise((resolve, reject) => {
const unzipper = new DecompressZip(settings.tempArchiveFile);
unzipper.on('error', reject);
unzipper.extract({
path: settings.workingPath,
strip: 1,
filter(file) {
return file.type !== 'SymbolicLink';
}
});
unzipper.on('extract', resolve);
});
}
export default async function extractZip(settings, logger) {
try {
logger.log('Extracting plugin archive');
await extractArchive(settings);
logger.log('Extraction complete');
} catch (err) {
logger.error(err);
throw new Error('Error extracting plugin archive');
}
};

View file

@ -1,14 +1,13 @@
var utils = require('requirefrom')('src/utils');
var fromRoot = utils('fromRoot');
const utils = require('requirefrom')('src/utils');
const fromRoot = utils('fromRoot');
const settingParser = require('./setting_parser');
const installer = require('./plugin_installer');
const remover = require('./plugin_remover');
const pluginLogger = require('./plugin_logger');
var settingParser = require('./settingParser');
var installer = require('./plugin_installer');
var remover = require('./pluginRemover');
var pluginLogger = require('./pluginLogger');
module.exports = function (program) {
export default function pluginCli(program) {
function processCommand(command, options) {
var settings;
let settings;
try {
settings = settingParser(command).parse();
} catch (ex) {
@ -17,7 +16,7 @@ module.exports = function (program) {
process.exit(64); // eslint-disable-line no-process-exit
}
var logger = pluginLogger(settings);
const logger = pluginLogger(settings);
if (settings.action === 'install') {
installer.install(settings, logger);
@ -54,14 +53,12 @@ module.exports = function (program) {
`
Common examples:
-i username/sample
attempts to download the latest version from the following urls:
attempts to download the latest version from the following url:
https://download.elastic.co/username/sample/sample-latest.tar.gz
https://github.com/username/sample/archive/master.tar.gz
-i username/sample/v1.1.1
attempts to download version v1.1.1 from the following urls:
attempts to download version v1.1.1 from the following url:
https://download.elastic.co/username/sample/sample-v1.1.1.tar.gz
https://github.com/username/sample/archive/v1.1.1.tar.gz
-i sample -u http://www.example.com/other_name.tar.gz
attempts to download from the specified url,

View file

@ -1,98 +0,0 @@
var _ = require('lodash');
var zlib = require('zlib');
var Promise = require('bluebird');
var url = require('url');
var fs = require('fs');
var request = require('request');
var tar = require('tar');
var progressReporter = require('./progressReporter');
module.exports = function (settings, logger) {
//Attempts to download each url in turn until one is successful
function download() {
var urls = settings.urls;
function tryNext() {
var sourceUrl = urls.shift();
if (!sourceUrl) {
throw new Error('Not a valid url.');
}
logger.log('Attempting to extract from ' + sourceUrl);
return Promise.try(function () {
return downloadSingle(sourceUrl, settings.workingPath, settings.timeout, logger)
.catch(function (err) {
if (err.message === 'ENOTFOUND') {
return tryNext();
}
if (err.message === 'EEXTRACT') {
throw (new Error('Error extracting the plugin archive... is this a valid tar.gz file?'));
}
throw (err);
});
})
.catch(function (err) {
//Special case for when request.get throws an exception
if (err.message.match(/invalid uri/i)) {
return tryNext();
}
throw (err);
});
}
return tryNext();
}
//Attempts to download a single url
function downloadSingle(source, dest, timeout) {
var gunzip = zlib.createGunzip();
var tarExtract = new tar.Extract({ path: dest, strip: 1 });
var requestOptions = { url: source };
if (timeout !== 0) {
requestOptions.timeout = timeout;
}
return wrappedRequest(requestOptions)
.then(function (fileStream) {
var reporter = progressReporter(logger, fileStream);
fileStream
.on('response', reporter.handleResponse)
.on('data', reporter.handleData)
.on('error', _.partial(reporter.handleError, 'ENOTFOUND'))
.pipe(gunzip)
.on('error', _.partial(reporter.handleError, 'EEXTRACT'))
.pipe(tarExtract)
.on('error', _.partial(reporter.handleError, 'EEXTRACT'))
.on('end', reporter.handleEnd);
return reporter.promise;
});
}
function wrappedRequest(requestOptions) {
return Promise.try(function () {
let urlInfo = url.parse(requestOptions.url);
if (/^file/.test(urlInfo.protocol)) {
return fs.createReadStream(urlInfo.path);
} else {
return request.get(requestOptions);
}
})
.catch(function (err) {
if (err.message.match(/invalid uri/i)) {
throw new Error('ENOTFOUND');
}
throw err;
});
}
return {
download: download,
_downloadSingle: downloadSingle
};
};

View file

@ -1,9 +1,7 @@
var rimraf = require('rimraf');
var fs = require('fs');
var Promise = require('bluebird');
module.exports = function (settings, logger) {
const rimraf = require('rimraf');
const fs = require('fs');
export default function createPluginCleaner(settings, logger) {
function cleanPrevious() {
return new Promise(function (resolve, reject) {
try {
@ -27,7 +25,6 @@ module.exports = function (settings, logger) {
function cleanError() {
// delete the working directory.
// At this point we're bailing, so swallow any errors on delete.
try {
rimraf.sync(settings.workingPath);
rimraf.sync(settings.pluginPath);

View file

@ -0,0 +1,51 @@
const _ = require('lodash');
const urlParse = require('url').parse;
const downloadHttpFile = require('./downloaders/http');
const downloadLocalFile = require('./downloaders/file');
export default function createPluginDownloader(settings, logger) {
let archiveType;
let sourceType;
//Attempts to download each url in turn until one is successful
function download() {
const urls = settings.urls.slice(0);
function tryNext() {
const sourceUrl = urls.shift();
if (!sourceUrl) {
throw new Error('No valid url specified.');
}
logger.log(`Attempting to transfer from ${sourceUrl}`);
return downloadSingle(sourceUrl)
.catch((err) => {
if (err.message === 'ENOTFOUND') {
return tryNext();
}
throw (err);
});
}
return tryNext();
}
function downloadSingle(sourceUrl) {
const urlInfo = urlParse(sourceUrl);
let downloadPromise;
if (/^file/.test(urlInfo.protocol)) {
downloadPromise = downloadLocalFile(logger, urlInfo.path, settings.tempArchiveFile);
} else {
downloadPromise = downloadHttpFile(logger, sourceUrl, settings.tempArchiveFile, settings.timeout);
}
return downloadPromise;
}
return {
download: download,
_downloadSingle: downloadSingle
};
};

View file

@ -0,0 +1,15 @@
const zipExtract = require('./extractors/zip');
const tarGzExtract = require('./extractors/tar_gz');
export default function extractArchive(settings, logger, archiveType) {
switch (archiveType) {
case '.zip':
return zipExtract(settings, logger);
break;
case '.tar.gz':
return tarGzExtract(settings, logger);
break;
default:
throw new Error('Unsupported archive format.');
}
};

View file

@ -1,72 +1,87 @@
let _ = require('lodash');
var utils = require('requirefrom')('src/utils');
var fromRoot = utils('fromRoot');
var pluginDownloader = require('./pluginDownloader');
var pluginCleaner = require('./plugin_cleaner');
var KbnServer = require('../../server/KbnServer');
var readYamlConfig = require('../serve/read_yaml_config');
var fs = require('fs');
const _ = require('lodash');
const utils = require('requirefrom')('src/utils');
const fromRoot = utils('fromRoot');
const pluginDownloader = require('./plugin_downloader');
const pluginCleaner = require('./plugin_cleaner');
const pluginExtractor = require('./plugin_extractor');
const KbnServer = require('../../server/KbnServer');
const readYamlConfig = require('../serve/read_yaml_config');
const { statSync, renameSync } = require('fs');
const Promise = require('bluebird');
const rimrafSync = require('rimraf').sync;
const mkdirp = Promise.promisify(require('mkdirp'));
module.exports = {
export default {
install: install
};
function install(settings, logger) {
logger.log(`Installing ${settings.package}`);
function checkForExistingInstall(settings, logger) {
try {
fs.statSync(settings.pluginPath);
statSync(settings.pluginPath);
logger.error(`Plugin ${settings.package} already exists, please remove before installing a new version`);
process.exit(70); // eslint-disable-line no-process-exit
} catch (e) {
if (e.code !== 'ENOENT') throw e;
}
}
var cleaner = pluginCleaner(settings, logger);
var downloader = pluginDownloader(settings, logger);
return cleaner.cleanPrevious()
.then(function () {
return downloader.download();
})
.then(function () {
fs.renameSync(settings.workingPath, settings.pluginPath);
})
.then(async function() {
logger.log('Optimizing and caching browser bundles...');
let serverConfig = _.merge(
readYamlConfig(settings.config),
{
env: 'production',
logging: {
silent: settings.silent,
quiet: !settings.silent,
verbose: false
},
optimize: {
useBundleCache: false
},
server: {
autoListen: false
},
plugins: {
initialize: false,
scanDirs: [settings.pluginDir, fromRoot('src/plugins')]
}
async function rebuildKibanaCache(settings, logger) {
logger.log('Optimizing and caching browser bundles...');
const serverConfig = _.merge(
readYamlConfig(settings.config),
{
env: 'production',
logging: {
silent: settings.silent,
quiet: !settings.silent,
verbose: false
},
optimize: {
useBundleCache: false
},
server: {
autoListen: false
},
plugins: {
initialize: false,
scanDirs: [settings.pluginDir, fromRoot('src/plugins')]
}
);
}
);
const kbnServer = new KbnServer(serverConfig);
await kbnServer.ready();
await kbnServer.close();
}
async function install(settings, logger) {
logger.log(`Installing ${settings.package}`);
const cleaner = pluginCleaner(settings, logger);
try {
checkForExistingInstall(settings, logger);
await cleaner.cleanPrevious();
await mkdirp(settings.workingPath);
const downloader = pluginDownloader(settings, logger);
const { archiveType } = await downloader.download();
await pluginExtractor (settings, logger, archiveType);
rimrafSync(settings.tempArchiveFile);
renameSync(settings.workingPath, settings.pluginPath);
await rebuildKibanaCache(settings, logger);
let kbnServer = new KbnServer(serverConfig);
await kbnServer.ready();
await kbnServer.close();
})
.then(function () {
logger.log('Plugin installation complete');
})
.catch(function (e) {
logger.error(`Plugin installation was unsuccessful due to error "${e.message}"`);
} catch (err) {
logger.error(`Plugin installation was unsuccessful due to error "${err.message}"`);
cleaner.cleanError();
process.exit(70); // eslint-disable-line no-process-exit
});
}
}

View file

@ -1,7 +1,7 @@
module.exports = function (settings) {
var previousLineEnded = true;
var silent = !!settings.silent;
var quiet = !!settings.quiet;
export default function createPluginLogger(settings) {
let previousLineEnded = true;
const silent = !!settings.silent;
const quiet = !!settings.quiet;
function log(data, sameLine) {
if (silent || quiet) return;
@ -33,7 +33,7 @@ module.exports = function (settings) {
data.pipe(process.stderr);
return;
}
process.stderr.write(data + '\n');
process.stderr.write(`${data}\n`);
previousLineEnded = true;
}

View file

@ -1,5 +1,5 @@
var fs = require('fs');
var rimraf = require('rimraf');
const fs = require('fs');
const rimraf = require('rimraf');
module.exports = {
remove: remove

View file

@ -1,71 +0,0 @@
var Promise = require('bluebird');
/*
Responsible for reporting the progress of the file stream
*/
module.exports = function (logger, stream) {
var oldDotCount = 0;
var runningTotal = 0;
var totalSize = 0;
var hasError = false;
var _resolve;
var _reject;
var _resp;
var promise = new Promise(function (resolve, reject) {
_resolve = resolve;
_reject = reject;
});
function handleError(errorMessage, err) {
if (hasError) return;
if (err) logger.error(err);
hasError = true;
if (stream.abort) stream.abort();
_reject(new Error(errorMessage));
}
function handleResponse(resp) {
_resp = resp;
if (resp.statusCode >= 400) {
handleError('ENOTFOUND', null);
} else {
totalSize = parseInt(resp.headers['content-length'], 10) || 0;
var totalDesc = totalSize || 'unknown number of';
logger.log('Downloading ' + totalDesc + ' bytes', true);
}
}
//Should log a dot for every 5% of progress
//Note: no progress is logged if the plugin is downloaded in a single packet
function handleData(buffer) {
if (hasError) return;
if (!totalSize) return;
runningTotal += buffer.length;
var dotCount = Math.round(runningTotal / totalSize * 100 / 5);
if (dotCount > 20) dotCount = 20;
for (var i = 0; i < (dotCount - oldDotCount); i++) {
logger.log('.', true);
}
oldDotCount = dotCount;
}
function handleEnd() {
if (hasError) return;
logger.log('Extraction complete');
_resolve();
}
return {
promise: promise,
handleResponse: handleResponse,
handleError: handleError,
handleData: handleData,
handleEnd: handleEnd,
hasError: function () { return hasError; }
};
};

View file

@ -0,0 +1,38 @@
/*
Generates file transfer progress messages
*/
export default function createProgressReporter(logger) {
let dotCount = 0;
let runningTotal = 0;
let totalSize = 0;
function init(size) {
totalSize = size;
let totalDesc = totalSize || 'unknown number of';
logger.log(`Transferring ${totalDesc} bytes`, true);
}
//Should log a dot for every 5% of progress
function progress(size) {
if (!totalSize) return;
runningTotal += size;
let newDotCount = Math.round(runningTotal / totalSize * 100 / 5);
if (newDotCount > 20) newDotCount = 20;
for (let i = 0; i < (newDotCount - dotCount); i++) {
logger.log('.', true);
}
dotCount = newDotCount;
}
function complete() {
logger.log(`Transfer complete`, false);
}
return {
init: init,
progress: progress,
complete: complete
};
};

View file

@ -1,12 +1,12 @@
var { resolve } = require('path');
var expiry = require('expiry-js');
const { resolve } = require('path');
const expiry = require('expiry-js');
module.exports = function (options) {
export default function createSettingParser(options) {
function parseMilliseconds(val) {
var result;
let result;
try {
var timeVal = expiry(val);
let timeVal = expiry(val);
result = timeVal.asMilliseconds();
} catch (ex) {
result = 0;
@ -16,22 +16,15 @@ module.exports = function (options) {
}
function generateDownloadUrl(settings) {
var version = (settings.version) || 'latest';
var filename = settings.package + '-' + version + '.tar.gz';
const version = (settings.version) || 'latest';
const filename = settings.package + '-' + version + '.tar.gz';
return 'https://download.elastic.co/' + settings.organization + '/' + settings.package + '/' + filename;
}
function generateGithubUrl(settings) {
var version = (settings.version) || 'master';
var filename = version + '.tar.gz';
return 'https://github.com/' + settings.organization + '/' + settings.package + '/archive/' + filename;
}
function parse() {
var parts;
var settings = {
let parts;
let settings = {
timeout: 0,
silent: false,
quiet: false,
@ -78,7 +71,6 @@ module.exports = function (options) {
settings.version = parts.shift();
settings.urls.push(generateDownloadUrl(settings));
settings.urls.push(generateGithubUrl(settings));
}
}
@ -100,6 +92,7 @@ module.exports = function (options) {
if (settings.package) {
settings.pluginPath = resolve(settings.pluginDir, settings.package);
settings.workingPath = resolve(settings.pluginDir, '.plugin.installing');
settings.tempArchiveFile = resolve(settings.workingPath, 'archive.part');
}
return settings;

View file

@ -60,7 +60,7 @@ describe('plugins/elasticsearch', function () {
});
});
it('should be created with 1 shard and 1 replica', function () {
it('should be created with 1 shard and default replica', function () {
var fn = createKibanaIndex(server);
return fn.then(function () {
var params = client.indices.create.args[0][0];
@ -71,22 +71,7 @@ describe('plugins/elasticsearch', function () {
expect(params.body.settings)
.to.have.property('number_of_shards', 1);
expect(params.body.settings)
.to.have.property('number_of_replicas', 1);
});
});
it('should be created with 1 shard and 1 replica', function () {
var fn = createKibanaIndex(server);
return fn.then(function () {
var params = client.indices.create.args[0][0];
expect(params)
.to.have.property('body');
expect(params.body)
.to.have.property('settings');
expect(params.body.settings)
.to.have.property('number_of_shards', 1);
expect(params.body.settings)
.to.have.property('number_of_replicas', 1);
.to.not.have.property('number_of_replicas');
});
});
@ -137,4 +122,3 @@ describe('plugins/elasticsearch', function () {
});
});

View file

@ -76,7 +76,7 @@ describe('plugins/elasticsearch', function () {
testRoute({
method: 'POST',
url: '/elasticsearch/.kibana',
payload: {settings: { number_of_shards: 1, number_of_replicas: 1 }},
payload: {settings: { number_of_shards: 1 }},
statusCode: 200
});

View file

@ -14,8 +14,7 @@ module.exports = function (server) {
index: index,
body: {
settings: {
number_of_shards: 1,
number_of_replicas: 1
number_of_shards: 1
},
mappings: {
config: {

View file

@ -17,7 +17,8 @@ module.exports = function (kibana) {
main: 'plugins/kibana/kibana',
uses: [
'visTypes',
'spyModes'
'spyModes',
'fieldFormats'
],
autoload: kibana.autoload.require.concat(

View file

@ -10,6 +10,7 @@ define(function (require) {
require('ui/config');
require('ui/notify');
require('ui/typeahead');
require('ui/share');
require('plugins/kibana/dashboard/directives/grid');
require('plugins/kibana/dashboard/components/panel/panel');
@ -233,15 +234,7 @@ define(function (require) {
ui: $state.options,
save: $scope.save,
addVis: $scope.addVis,
addSearch: $scope.addSearch,
shareData: function () {
return {
link: $location.absUrl(),
// This sucks, but seems like the cleanest way. Uhg.
embed: '<iframe src="' + $location.absUrl().replace('?', '?embed&') +
'" height="600" width="800"></iframe>'
};
}
addSearch: $scope.addSearch
};
init();

View file

@ -1,21 +1,4 @@
<form role="form" class="vis-share">
<p>
<div class="input-group">
<label>
Embed this dashboard
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{opts.shareData().embed}}</div>
</div>
</p>
<p>
<div class="input-group">
<label>
Share a link
</label>
<div class="form-control" disabled>{{opts.shareData().link}}</div>
</div>
</p>
</form>
<share
object-type="dashboard"
object-id="{{opts.dashboard.id}}">
</share>

View file

@ -11,7 +11,6 @@ define(function (require) {
require('ui/doc_table');
require('ui/visualize');
require('ui/notify');
require('ui/timepicker');
require('ui/fixedScroll');
require('ui/directives/validate_json');
require('ui/filters/moment');
@ -20,6 +19,7 @@ define(function (require) {
require('ui/state_management/app_state');
require('ui/timefilter');
require('ui/highlight/highlight_tags');
require('ui/share');
var app = require('ui/modules').get('apps/discover', [
'kibana/notify',
@ -91,7 +91,8 @@ define(function (require) {
// config panel templates
$scope.configTemplate = new ConfigTemplate({
load: require('plugins/kibana/discover/partials/load_search.html'),
save: require('plugins/kibana/discover/partials/save_search.html')
save: require('plugins/kibana/discover/partials/save_search.html'),
share: require('plugins/kibana/discover/partials/share_search.html')
});
$scope.timefilter = timefilter;

View file

@ -50,6 +50,16 @@
<i aria-hidden="true" class="fa fa-folder-open-o"></i>
</button>
</kbn-tooltip>
<kbn-tooltip text="Share" placement="bottom" append-to-body="1">
<button
aria-label="Share Search"
aria-haspopup="true"
aria-expanded="{{ configTemplate.is('share') }}"
ng-class="{active: configTemplate.is('share')}"
ng-click="configTemplate.toggle('share');">
<i aria-hidden="true" class="fa fa-external-link"></i>
</button>
</kbn-tooltip>
</div>
</navbar>

View file

@ -0,0 +1,5 @@
<share
object-type="search"
object-id="{{opts.savedSearch.id}}"
allow-embed="false">
</share>

View file

@ -3,6 +3,7 @@ require('plugins/kibana/visualize/index');
require('plugins/kibana/dashboard/index');
require('plugins/kibana/settings/index');
require('plugins/kibana/doc/index');
require('ui/timepicker');
var moment = require('moment-timezone');
@ -12,6 +13,8 @@ var modules = require('ui/modules');
var kibanaLogoUrl = require('ui/images/kibana.svg');
routes.enable();
routes
.otherwise({
redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}`

View file

@ -73,6 +73,36 @@
</small>
</div>
<div class="form-group" ng-if="canExpandIndices()">
<label>
<input ng-model="index.notExpandable" type="checkbox">
Do not expand index pattern when searching <small>(Not recommended)</small>
</label>
<div ng-if="index.notExpandable" class="alert alert-info">
This index pattern will be queried directly rather than being
expanded into more performant searches against individual indices.
Elasticsearch will receive a query against <em>{{index.name}}</em>
and will have to search through all matching indices regardless
of whether they have data that matches the current time range.
</div>
<p class="help-block">
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.
</p>
<p class="help-block">
Searching against the index pattern <em>logstash-*</em> will
actually query elasticsearch for the specific matching indices
(e.g. <em>logstash-2015.12.21</em>) that fall within the current
time range.
</p>
</div>
<section>
<div class="alert alert-danger" ng-repeat="err in index.patternErrors">
{{err}}

View file

@ -24,6 +24,7 @@ define(function (require) {
isTimeBased: true,
nameIsPattern: false,
notExpandable: false,
sampleCount: 5,
nameIntervalOptions: intervals,
@ -33,6 +34,12 @@ define(function (require) {
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);
};
@ -50,6 +57,10 @@ define(function (require) {
}
}
if (index.notExpandable && $scope.canExpandIndices()) {
indexPattern.notExpandable = true;
}
// fetch the fields
return indexPattern.create()
.then(function (id) {

View file

@ -22,6 +22,10 @@
<div ng-if="indexPattern.timeFieldName && indexPattern.intervalName" class="alert alert-info">
This index uses a <strong>Time-based index pattern</strong> which repeats <span ng-bind="::indexPattern.getInterval().display"></span>
</div>
<div ng-if="!indexPattern.canExpandIndices()" class="alert alert-info">
This index pattern is set to be queried directly rather than being
expanded into more performant searches against individual indices.
</div>
<div ng-if="conflictFields.length" class="alert alert-warning">
<strong>Mapping conflict!</strong> {{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.
</div>

View file

@ -6,6 +6,7 @@ define(function (require) {
require('ui/visualize');
require('ui/collapsible_sidebar');
require('ui/share');
require('ui/routes')
.when('/visualize/create', {
@ -234,15 +235,6 @@ define(function (require) {
}, notify.fatal);
};
$scope.shareData = function () {
return {
link: $location.absUrl(),
// This sucks, but seems like the cleanest way. Uhg.
embed: '<iframe src="' + $location.absUrl().replace('?', '?embed&') +
'" height="600" width="800"></iframe>'
};
};
$scope.unlink = function () {
if (!$state.linked) return;

View file

@ -1,22 +1,4 @@
<form role="form" class="vis-share">
<p>
<div class="form-group">
<label>
Embed this visualization.
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<div class="form-control" disabled>{{conf.shareData().embed}}</div>
</div>
</p>
<p>
<div class="form-group">
<label>
Share a link
</label>
<div class="form-control" disabled>{{conf.shareData().link}}</div>
</div>
</p>
</form>
<share
object-type="visualization"
object-id="{{conf.savedVis.id}}">
</share>

View file

@ -10,6 +10,8 @@ module.exports = function (kbnServer, server, config) {
server = kbnServer.server = new Hapi.Server();
const shortUrlLookup = require('./short_url_lookup')(server);
// Create a new connection
var connectionOptions = {
host: config.get('server.host'),
@ -154,5 +156,23 @@ module.exports = function (kbnServer, server, config) {
}
});
server.route({
method: 'GET',
path: '/goto/{urlId}',
handler: async function (request, reply) {
const url = await shortUrlLookup.getUrl(request.params.urlId);
reply().redirect(url);
}
});
server.route({
method: 'POST',
path: '/shorten',
handler: async function (request, reply) {
const urlId = await shortUrlLookup.generateUrlId(request.payload.url);
reply(urlId);
}
});
return kbnServer.mixin(require('./xsrf'));
};

View file

@ -0,0 +1,101 @@
const crypto = require('crypto');
export default function (server) {
async function updateMetadata(urlId, urlDoc) {
const client = server.plugins.elasticsearch.client;
try {
await client.update({
index: '.kibana',
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;
client.get({
index: '.kibana',
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;
client.index({
index: '.kibana',
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 note exist in kibana index');
updateMetadata(urlId, urlDoc);
return urlDoc._source.url;
} catch (err) {
return '/';
}
}
};
};

View file

@ -18,7 +18,7 @@ class UiApp {
this.icon = this.spec.icon;
this.hidden = this.spec.hidden;
this.autoloadOverrides = this.spec.autoload;
this.templateName = this.spec.templateName || 'uiApp';
this.templateName = this.spec.templateName || 'ui_app';
this.url = `${spec.urlBasePath || ''}${this.spec.url || `/app/${this.id}`}`;
// once this resolves, no reason to run it again
@ -32,6 +32,7 @@ class UiApp {
return _.chain([
this.autoloadOverrides || autoload.require,
this.uiExports.find(_.get(this, 'spec.uses', [])),
this.uiExports.find(['chromeNavControls']),
])
.flatten()
.uniq()

View file

@ -60,6 +60,7 @@ class UiExports {
case 'visTypes':
case 'fieldFormats':
case 'spyModes':
case 'chromeNavControls':
return (plugin, spec) => {
this.aliases[type] = _.union(this.aliases[type] || [], spec);
};

View file

@ -63,7 +63,7 @@ exports.reload = function () {
'ui/stringify/register',
'ui/styleCompile',
'ui/timefilter',
'ui/timepicker',
'ui/timepicker', // TODO: remove this for 5.0
'ui/tooltip',
'ui/typeahead',
'ui/url',

View file

@ -0,0 +1,73 @@
import ngMock from 'ngMock';
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 = $('<div kbn-chrome-append-nav-controls>');
$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: `<span id="testTemplateEl"></span>`
};
});
var $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: `<span id="2", class="testControl"></span>`
};
});
stubRegistry.register(function () {
return {
name: 'control1',
order: 1,
template: `<span id="1", class="testControl"></span>`
};
});
stubRegistry.register(function () {
return {
name: 'control3',
order: 3,
template: `<span id="3", class="testControl"></span>`
};
});
var $el = compile();
expect(
$el.find('.testControl')
.toArray()
.map(el => el.id)
).to.eql(['3', '2', '1']);
});
});

View file

@ -1,13 +1,9 @@
var $ = require('jquery');
var _ = require('lodash');
require('../appSwitcher');
var modules = require('ui/modules');
var ConfigTemplate = require('ui/ConfigTemplate');
require('ui/directives/config');
module.exports = function (chrome, internals) {
chrome.setupAngular = function () {
var modules = require('ui/modules');
var kibana = modules.get('kibana');
_.forOwn(chrome.getInjected(), function (val, name) {
@ -24,53 +20,9 @@ module.exports = function (chrome, internals) {
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');
.config(chrome.$setupXsrfRequestInterceptor);
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);
};
$rootScope.$on('$routeChangeSuccess', onRouteChange);
$rootScope.$on('$routeUpdate', onRouteChange);
onRouteChange();
// and some local values
$scope.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;
$scope.appSwitcherTemplate = new ConfigTemplate({
switcher: '<app-switcher></app-switcher>'
});
return chrome;
}
};
});
require('../directives')(chrome, internals);
modules.link(kibana);
};

View file

@ -22,7 +22,7 @@
<!-- /Mobile navbar -->
<!-- Full navbar -->
<div collapse="!showCollapsed" class="navbar-collapse">
<div collapse="!showCollapsed" class="navbar-collapse" kbn-chrome-append-nav-controls>
<ul class="nav navbar-nav" role="navigation">
<li
ng-if="chrome.getBrand('logo')"
@ -52,50 +52,11 @@
</a>
</li>
</ul>
<ul ng-show="timefilter.enabled" class="nav navbar-nav navbar-right navbar-timepicker">
<li>
<a
ng-click="toggleRefresh()"
ng-show="timefilter.refreshInterval.value > 0">
<i class="fa" ng-class="timefilter.refreshInterval.pause ? 'fa-play' : 'fa-pause'"></i>
</a>
</li>
<li
ng-class="{active: pickerTemplate.is('interval') }"
ng-show="timefilter.refreshInterval.value > 0 || !!pickerTemplate.current"
class="to-body">
<a ng-click="pickerTemplate.toggle('interval')" class="navbar-timepicker-auto-refresh-desc">
<span ng-show="timefilter.refreshInterval.value === 0"><i class="fa fa-repeat"></i> Auto-refresh</span>
<span ng-show="timefilter.refreshInterval.value > 0">{{timefilter.refreshInterval.display}}</span>
</a>
</li>
<li class="to-body" ng-class="{active: pickerTemplate.is('filter')}">
<a
ng-click="pickerTemplate.toggle('filter')"
aria-haspopup="true"
aria-expanded="false"
class="navbar-timepicker-time-desc">
<i aria-hidden="true" class="fa fa-clock-o"></i>
<pretty-duration from="timefilter.time.from" to="timefilter.time.to"></pretty-duration>
</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right navbar-timepicker" >
<li ng-show="httpActive.length" class="navbar-text hidden-xs">
<div class="spinner"></div>
</li>
</ul>
</div>
<!-- /Full navbar -->
</nav>
<!-- TODO: These config dropdowns shouldn't be hard coded -->
<config
config-template="appSwitcherTemplate"
config-object="chrome"

View file

@ -4,6 +4,7 @@ define(function (require) {
require('ui/modules')
.get('kibana')
// TODO: all of this really belongs in the timepicker
.directive('chromeContext', function (timefilter, globalState) {
var listenForUpdates = _.once(function ($scope) {

View file

@ -8,7 +8,7 @@ var cloneDeep = require('lodash').cloneDeep;
var indexBy = require('lodash').indexBy;
require('ui/chrome');
require('ui/chrome/appSwitcher');
require('../app_switcher');
var DomLocationProvider = require('ui/domLocation');
describe('appSwitcher directive', function () {

View file

@ -1,7 +1,7 @@
var parse = require('url').parse;
var bindKey = require('lodash').bindKey;
require('../appSwitcher/appSwitcher.less');
require('../app_switcher/app_switcher.less');
var DomLocationProvider = require('ui/domLocation');
require('ui/modules')
@ -9,7 +9,7 @@ require('ui/modules')
.directive('appSwitcher', function () {
return {
restrict: 'E',
template: require('./appSwitcher.html'),
template: require('./app_switcher.html'),
controllerAs: 'switcher',
controller: function ($scope, Private) {
var domLocation = Private(DomLocationProvider);

View file

@ -0,0 +1,28 @@
import $ from 'jquery';
import chromeNavControlsRegistry from 'ui/registry/chrome_nav_controls';
import UiModules from 'ui/modules';
export default function (chrome, internals) {
UiModules
.get('kibana')
.directive('kbnChromeAppendNavControls', function (Private) {
return {
template: function ($element) {
const parts = [$element.html()];
const controls = Private(chromeNavControlsRegistry);
for (const control of controls.inOrder) {
parts.unshift(
`<!-- nav control ${control.name} -->`,
control.template
);
}
return parts.join('\n');
}
};
});
}

View file

@ -0,0 +1,10 @@
import 'ui/directives/config';
import './app_switcher';
import kbnChromeProv from './kbn_chrome';
import kbnChromeNavControlsProv from './append_nav_controls';
export default function (chrome, internals) {
kbnChromeProv(chrome, internals);
kbnChromeNavControlsProv(chrome, internals);
}

View file

@ -0,0 +1,58 @@
import $ from 'jquery';
import UiModules from 'ui/modules';
import ConfigTemplate from 'ui/ConfigTemplate';
export default function (chrome, internals) {
UiModules
.get('kibana')
.directive('kbnChrome', function ($rootScope) {
return {
template($el) {
const $content = $(require('ui/chrome/chrome.html'));
const $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($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
const onRouteChange = function () {
let { href } = window.location;
let persist = chrome.getVisible();
internals.trackPossibleSubUrl(href);
internals.tabs.consumeRouteUpdate(href, persist);
};
$rootScope.$on('$routeChangeSuccess', onRouteChange);
$rootScope.$on('$routeUpdate', onRouteChange);
onRouteChange();
// and some local values
$scope.httpActive = $http.pendingRequests;
$scope.notifList = require('ui/notify')._notifs;
$scope.appSwitcherTemplate = new ConfigTemplate({
switcher: '<app-switcher></app-switcher>'
});
return chrome;
}
};
});
}

View file

@ -294,6 +294,84 @@ describe('index pattern', function () {
});
});
describe('#toDetailedIndexList', function () {
require('testUtils/noDigestPromises').activateForSuite();
context('when index pattern is an interval', function () {
var interval;
beforeEach(function () {
interval = 'result:getInterval';
sinon.stub(indexPattern, 'getInterval').returns(interval);
});
it('invokes interval toDetailedIndexList with given start/stop times', async function () {
await indexPattern.toDetailedIndexList(1, 2);
var id = indexPattern.id;
expect(intervals.toIndexList.calledWith(id, interval, 1, 2)).to.be(true);
});
it('is fulfilled by the result of interval toDetailedIndexList', async function () {
var indexList = await indexPattern.toDetailedIndexList();
expect(indexList[0].index).to.equal('foo');
expect(indexList[1].index).to.equal('bar');
});
context('with sort order', function () {
it('passes the sort order to the intervals module', function () {
return indexPattern.toDetailedIndexList(1, 2, 'SORT_DIRECTION')
.then(function () {
expect(intervals.toIndexList.callCount).to.be(1);
expect(intervals.toIndexList.getCall(0).args[4]).to.be('SORT_DIRECTION');
});
});
});
});
context('when index pattern is a time-base wildcard', function () {
beforeEach(function () {
sinon.stub(indexPattern, 'getInterval').returns(false);
sinon.stub(indexPattern, 'hasTimeField').returns(true);
sinon.stub(indexPattern, 'isWildcard').returns(true);
});
it('invokes calculateIndices with given start/stop times and sortOrder', async function () {
await indexPattern.toDetailedIndexList(1, 2, 'sortOrder');
var id = indexPattern.id;
var field = indexPattern.timeFieldName;
expect(calculateIndices.calledWith(id, field, 1, 2, 'sortOrder')).to.be(true);
});
it('is fulfilled by the result of calculateIndices', async function () {
var indexList = await indexPattern.toDetailedIndexList();
expect(indexList[0].index).to.equal('foo');
expect(indexList[1].index).to.equal('bar');
});
});
context('when index pattern is a time-base wildcard that is configured not to expand', function () {
beforeEach(function () {
sinon.stub(indexPattern, 'getInterval').returns(false);
sinon.stub(indexPattern, 'hasTimeField').returns(true);
sinon.stub(indexPattern, 'isWildcard').returns(true);
sinon.stub(indexPattern, 'canExpandIndices').returns(false);
});
it('is fulfilled by id', async function () {
var indexList = await indexPattern.toDetailedIndexList();
expect(indexList.index).to.equal(indexPattern.id);
});
});
context('when index pattern is neither an interval nor a time-based wildcard', function () {
beforeEach(function () {
sinon.stub(indexPattern, 'getInterval').returns(false);
});
it('is fulfilled by id', async function () {
var indexList = await indexPattern.toDetailedIndexList();
expect(indexList.index).to.equal(indexPattern.id);
});
});
});
describe('#toIndexList', function () {
context('when index pattern is an interval', function () {
require('testUtils/noDigestPromises').activateForSuite();
@ -348,6 +426,21 @@ describe('index pattern', function () {
});
});
context('when index pattern is a time-base wildcard that is configured not to expand', function () {
require('testUtils/noDigestPromises').activateForSuite();
beforeEach(function () {
sinon.stub(indexPattern, 'getInterval').returns(false);
sinon.stub(indexPattern, 'hasTimeField').returns(true);
sinon.stub(indexPattern, 'isWildcard').returns(true);
sinon.stub(indexPattern, 'canExpandIndices').returns(false);
});
it('is fulfilled by id', async function () {
var indexList = await indexPattern.toIndexList();
expect(indexList).to.equal(indexPattern.id);
});
});
context('when index pattern is neither an interval nor a time-based wildcard', function () {
beforeEach(function () {
sinon.stub(indexPattern, 'getInterval').returns(false);
@ -365,6 +458,21 @@ describe('index pattern', function () {
});
});
describe('#canExpandIndices()', function () {
it('returns true if notExpandable is false', function () {
indexPattern.notExpandable = false;
expect(indexPattern.canExpandIndices()).to.be(true);
});
it('returns true if notExpandable is not defined', function () {
delete indexPattern.notExpandable;
expect(indexPattern.canExpandIndices()).to.be(true);
});
it('returns false if notExpandable is true', function () {
indexPattern.notExpandable = true;
expect(indexPattern.canExpandIndices()).to.be(false);
});
});
describe('#hasTimeField()', function () {
beforeEach(function () {
// for the sake of these tests, it doesn't much matter what type of field

View file

@ -25,6 +25,7 @@ define(function (require) {
var mapping = mappingSetup.expandShorthand({
title: 'string',
timeFieldName: 'string',
notExpandable: 'boolean',
intervalName: 'string',
fields: 'json',
fieldFormatMap: {
@ -196,7 +197,7 @@ define(function (require) {
return intervals.toIndexList(self.id, interval, start, stop, sortDirection);
}
if (self.isWildcard() && self.hasTimeField()) {
if (self.isWildcard() && self.hasTimeField() && self.canExpandIndices()) {
return calculateIndices(self.id, self.timeFieldName, start, stop, sortDirection);
}
@ -207,6 +208,10 @@ define(function (require) {
};
});
self.canExpandIndices = function () {
return !this.notExpandable;
};
self.hasTimeField = function () {
return !!(this.timeFieldName && this.fields.byName[this.timeFieldName]);
};

View file

@ -1,9 +1,63 @@
/**
* This module is used by Kibana to create and reuse angular modules. Angular modules
* can only be created once and need to have their dependencies at creation. This is
* hard/impossible to do in require.js since all of the dependencies for a module are
* loaded before it is.
*
* Here is an example:
*
* In the scenario below, require.js would load directive.js first because it is a
* dependency of app.js. This would cause the call to `angular.module('app')` to
* execute before the module is actually created. This causes angular to through an
* error. This effect is magnifies when app.js links off to many different modules.
*
* This is normally solved by creating unique modules per file, listed as the 1st
* alternate solution below. Unfortunately this solution would have required that
* we replicate our require statements.
*
* app.js
* ```
* angular.module('app', ['ui.bootstrap'])
* .controller('AppController', function () { ... });
*
* require('./directive');
* ```
*
* directive.js
* ```
* angular.module('app')
* .directive('someDirective', function () { ... });
* ```
*
* Before taking this approach we saw three possible solutions:
* 1. replicate our js modules in angular modules/use a different module per file
* 2. create a single module outside of our js modules and share it
* 3. use a helper lib to dynamically create modules as needed.
*
* We decided to go with #3
*
* This ends up working by creating a list of modules that the code base creates by
* calling `modules.get(name)` with different names, and then before bootstrapping
* the application kibana uses `modules.link()` to set the dependencies of the "kibana"
* module to include every defined module. This guarantees that kibana can always find
* any angular dependecy defined in the kibana code base. This **also** means that
* Private modules are able to find any dependency, since they are injected using the
* "kibana" module's injector.
*
*/
define(function (require) {
var angular = require('angular');
var existingModules = {};
var _ = require('lodash');
var links = [];
/**
* Take an angular module and extends the dependencies for that module to include all of the modules
* created using `ui/modules`
*
* @param {AngularModule} module - the module to extend
* @return {undefined}
*/
function link(module) {
// as modules are defined they will be set as requirements for this app
links.push(module);
@ -12,6 +66,19 @@ define(function (require) {
module.requires = _.union(module.requires, _.keys(existingModules));
}
/**
* The primary means of interacting with `ui/modules`. Returns an angular module. If the module already
* exists the existing version will be returned. `dependencies` are either set as or merged into the
* modules total dependencies.
*
* This is in contrast to the `angular.module(name, [dependencies])` function which will only
* create a module if the `dependencies` list is passed and get an existing module if no dependencies
* are passed. This requires knowing the order that your files will load, which we can't guarantee.
*
* @param {string} moduleName - the unique name for this module
* @param {array[string]} [requires=[]] - the other modules this module requires
* @return {AngularModule}
*/
function get(moduleName, requires) {
var module = existingModules[moduleName];

View file

@ -1,6 +1,9 @@
<div class="pagination-other-pages">
<ul class="pagination-other-pages-list pagination-sm" ng-if="page.count > 1">
<li ng-hide="page.first">
<li ng-style="{'visibility':'hidden'}" ng-if="page.first">
<a ng-click="paginate.goToPage(page.prev)">«</a>
</li>
<li ng-style="{'visibility':'visible'}" ng-if="!page.first">
<a ng-click="paginate.goToPage(page.prev)">«</a>
</li>
@ -18,7 +21,10 @@
<a ng-click="paginate.goToPage(page.count)">...{{page.count}}</a>
</li>
<li ng-hide="page.last">
<li ng-style="{'visibility':'hidden'}" ng-if="page.last">
<a ng-click="paginate.goToPage(page.next)">»</a>
</li>
<li ng-style="{'visibility':'visible'}" ng-if="!page.last">
<a ng-click="paginate.goToPage(page.next)">»</a>
</li>
</ul>

View file

@ -0,0 +1,6 @@
define(function (require) {
return require('ui/registry/_registry')({
name: 'chromeNavControls',
order: ['order']
});
});

View file

@ -1,8 +1,12 @@
var RouteManager = require('./RouteManager');
var defaultRouteManager = new RouteManager();
require('ui/modules')
.get('kibana', ['ngRoute'])
.config(defaultRouteManager.config);
module.exports = defaultRouteManager;
module.exports = {
...defaultRouteManager,
enable() {
require('angular-route/angular-route');
require('ui/modules')
.get('kibana', ['ngRoute'])
.config(defaultRouteManager.config);
}
};

View file

@ -0,0 +1,16 @@
const app = require('ui/modules').get('kibana');
app.directive('share', function () {
return {
restrict: 'E',
scope: {
objectType: '@',
objectId: '@',
setAllowEmbed: '&?allowEmbed'
},
template: require('ui/share/views/share.html'),
controller: function ($scope) {
$scope.allowEmbed = $scope.setAllowEmbed ? $scope.setAllowEmbed() : true;
}
};
});

View file

@ -0,0 +1,76 @@
const app = require('ui/modules').get('kibana');
const Clipboard = require('clipboard');
require('../styles/index.less');
app.directive('shareObjectUrl', function (Private, Notifier) {
const urlShortener = Private(require('../lib/url_shortener'));
return {
restrict: 'E',
scope: {
getShareAsEmbed: '&shareAsEmbed'
},
template: require('ui/share/views/share_object_url.html'),
link: function ($scope, $el) {
const notify = new Notifier({
location: `Share ${$scope.$parent.objectType}`
});
$scope.textbox = $el.find('input.url')[0];
$scope.clipboardButton = $el.find('button.clipboard-button')[0];
const clipboard = new Clipboard($scope.clipboardButton, {
target(trigger) {
return $scope.textbox;
}
});
clipboard.on('success', e => {
notify.info('URL copied to clipboard.');
e.clearSelection();
});
clipboard.on('error', () => {
notify.info('URL selected. Press Ctrl+C to copy.');
});
$scope.$on('$destroy', () => {
clipboard.destroy();
});
$scope.clipboard = clipboard;
},
controller: function ($scope, $location) {
function updateUrl(url) {
$scope.url = url;
if ($scope.shareAsEmbed) {
$scope.formattedUrl = `<iframe src="${$scope.url}" height="600" width="800"></iframe>`;
} else {
$scope.formattedUrl = $scope.url;
}
$scope.shortGenerated = false;
}
$scope.shareAsEmbed = $scope.getShareAsEmbed();
$scope.generateShortUrl = function () {
if ($scope.shortGenerated) return;
urlShortener.shortenUrl($scope.url)
.then(shortUrl => {
updateUrl(shortUrl);
$scope.shortGenerated = true;
});
};
$scope.getUrl = function () {
return $location.absUrl();
};
$scope.$watch('getUrl()', updateUrl);
}
};
});

View file

@ -0,0 +1,2 @@
require('./directives/share');
require('./directives/share_object_url');

View file

@ -0,0 +1,24 @@
export default function createUrlShortener(Notifier, $http, $location) {
const notify = new Notifier({
location: 'Url Shortener'
});
const baseUrl = `${$location.protocol()}://${$location.host()}:${$location.port()}`;
async function shortenUrl(url) {
const relativeUrl = url.replace(baseUrl, '');
const formData = { url: relativeUrl };
try {
const result = await $http.post('/shorten', formData);
return `${baseUrl}/goto/${result.data}`;
} catch (err) {
notify.error(err);
throw err;
}
}
return {
shortenUrl
};
};

View file

@ -0,0 +1,21 @@
share-object-url {
.input-group {
display: flex;
.clipboard-button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.shorten-button {
border-top-right-radius: 0;
border-top-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-control.url {
cursor: text;
}
}
}

View file

@ -0,0 +1,15 @@
<form role="form" class="vis-share">
<div class="form-group" ng-if="allowEmbed">
<label>
Embed this {{objectType}}
<small>Add to your html source. Note all clients must still be able to access kibana</small>
</label>
<share-object-url share-as-embed="true"></share-object-url>
</div>
<div class="form-group">
<label>
Share a link
</label>
<share-object-url share-as-embed="false"></share-object-url>
</div>
</form>

View file

@ -0,0 +1,21 @@
<div class="input-group">
<input
ng-model="formattedUrl"
type="text"
readonly=""
class="form-control url">
</input>
<button
class="shorten-button"
tooltip="Generate Short URL"
ng-click="generateShortUrl()"
ng-disabled="shortGenerated">
<span aria-hidden="true" class="fa fa-compress"></span>
</button>
<button
class="clipboard-button"
tooltip="Copy to Clipboard"
ng-click="copyToClipboard()">
<span aria-hidden="true" class="fa fa-clipboard"></span>
</button>
</div>

View file

@ -10,6 +10,7 @@ define(function (require) {
require('ui/timepicker/quick_ranges');
require('ui/timepicker/refresh_intervals');
require('ui/timepicker/time_units');
require('ui/timepicker/toggle');
module.directive('kbnTimepicker', function (quickRanges, timeUnits, refreshIntervals) {
return {

View file

@ -0,0 +1,33 @@
<ul ng-show="timefilter.enabled" class="nav navbar-nav navbar-right navbar-timepicker">
<li>
<a
ng-click="toggleRefresh()"
ng-show="timefilter.refreshInterval.value > 0">
<i class="fa" ng-class="timefilter.refreshInterval.pause ? 'fa-play' : 'fa-pause'"></i>
</a>
</li>
<li
ng-class="{active: pickerTemplate.is('interval') }"
ng-show="timefilter.refreshInterval.value > 0 || !!pickerTemplate.current"
class="to-body">
<a ng-click="pickerTemplate.toggle('interval')" class="navbar-timepicker-auto-refresh-desc">
<span ng-show="timefilter.refreshInterval.value === 0"><i class="fa fa-repeat"></i> Auto-refresh</span>
<span ng-show="timefilter.refreshInterval.value > 0">{{timefilter.refreshInterval.display}}</span>
</a>
</li>
<li class="to-body" ng-class="{active: pickerTemplate.is('filter')}">
<a
ng-click="pickerTemplate.toggle('filter')"
aria-haspopup="true"
aria-expanded="false"
class="navbar-timepicker-time-desc">
<i aria-hidden="true" class="fa fa-clock-o"></i>
<pretty-duration from="timefilter.time.from" to="timefilter.time.to"></pretty-duration>
</a>
</li>
</ul>

View file

@ -0,0 +1,16 @@
import UiModules from 'ui/modules';
import chromeNavControlsRegistry from 'ui/registry/chrome_nav_controls';
import toggleHtml from './toggle.html';
// TODO: the chrome-context directive is currently responsible for several variables
// on scope used by this template. We need to get rid of that directive and move that
// logic here
chromeNavControlsRegistry.register(function () {
return {
name: 'timepicker toggle',
order: 100,
template: toggleHtml
};
});

View file

@ -0,0 +1,41 @@
const _ = require('lodash');
const ngMock = require('ngMock');
const expect = require('expect.js');
let mappingSetup;
describe('ui/utils/mapping_setup', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
mappingSetup = Private(require('ui/utils/mapping_setup'));
}));
describe('#expandShorthand()', function () {
it('allows shortcuts for field types by just setting the value to the type name', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'boolean' });
expect(mapping.foo.type).to.be('boolean');
});
it('can set type as an option', function () {
const mapping = mappingSetup.expandShorthand({ foo: {type: 'integer'} });
expect(mapping.foo.type).to.be('integer');
});
context('when type is json', function () {
it('returned object is type string', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
expect(mapping.foo.type).to.be('string');
});
it('returned object has _serialize function', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
expect(_.isFunction(mapping.foo._serialize)).to.be(true);
});
it('returned object has _deserialize function', function () {
const mapping = mappingSetup.expandShorthand({ foo: 'json' });
expect(_.isFunction(mapping.foo._serialize)).to.be(true);
});
});
});
});

View file

@ -38,12 +38,13 @@ block content
loading.removeChild(loading.lastChild);
}
var buildNum = #{kibanaPayload.buildNum};
var cacheParam = buildNum ? '?v=' + buildNum : '';
function bundleFile(filename) {
var anchor = document.createElement('a');
anchor.setAttribute('href', !{JSON.stringify(bundlePath)} + '/' + filename);
anchor.setAttribute('href', !{JSON.stringify(bundlePath)} + '/' + filename + cacheParam);
return anchor.href;
}
var files = [
bundleFile('commons.style.css'),
bundleFile('#{app.id}.style.css'),
@ -55,7 +56,7 @@ block content
var file = files.shift();
if (!file) return;
var type = /\.js$/.test(file) ? 'script' : 'link';
var type = /\.js(\?.+)?$/.test(file) ? 'script' : 'link';
var dom = document.createElement(type);
dom.setAttribute('async', '');

View file

@ -5,9 +5,9 @@ module.exports = function (grunt) {
return {
options: {
selenium: {
filename: 'selenium-server-standalone-2.47.1.jar',
server: 'https://selenium-release.storage.googleapis.com/2.47/',
md5: 'e6cb10b8f0f353c6ca4a8f62fb5cb472',
filename: 'selenium-server-standalone-2.48.2.jar',
server: 'https://selenium-release.storage.googleapis.com/2.48/',
md5: 'b2784fc67c149d3c13c83d2108104689',
directory: path.join(grunt.config.get('root'), 'selenium')
}
}

View file

@ -88,7 +88,7 @@ module.exports = function (grunt) {
cmd: 'java',
args: [
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'selenium/selenium-server-standalone-2.48.2.jar',
'-port',
uiConfig.servers.webdriver.port
]
@ -104,7 +104,7 @@ module.exports = function (grunt) {
cmd: 'java',
args: [
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'selenium/selenium-server-standalone-2.48.2.jar',
'-port',
uiConfig.servers.webdriver.port
]

View file

@ -1,7 +1,6 @@
module.exports = {
settings: {
number_of_shards: 1,
number_of_replicas: 1
number_of_shards: 1
},
mappings: {
config: {

View file

@ -5,7 +5,7 @@ define(function (require) {
return _.assign({
debug: true,
capabilities: {
'selenium-version': '2.47.1',
'selenium-version': '2.48.2',
'idle-timeout': 99
},
environments: [{

View file

@ -10,12 +10,14 @@ module.exports = {
kibana: {
protocol: process.env.TEST_UI_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_UI_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620,
auth: 'user:notsecure'
},
elasticsearch: {
protocol: process.env.TEST_UI_ES_PROTOCOL || 'http',
hostname: process.env.TEST_UI_ES_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220,
auth: 'admin:notsecure'
}
},
apps: {

View file

@ -6,12 +6,46 @@ define(function (require) {
var testSubjSelector = require('intern/dojo/node!@spalger/test-subj-selector');
var getUrl = require('intern/dojo/node!../../utils/getUrl');
var fs = require('intern/dojo/node!fs');
var _ = require('intern/dojo/node!lodash');
var parse = require('intern/dojo/node!url').parse;
var format = require('intern/dojo/node!url').format;
var path = require('intern/dojo/node!path');
function injectTimestampQuery(func, url) {
var formatted = modifyQueryString(url, function (parsed) {
parsed.query._t = Date.now();
});
return func.call(this, formatted);
}
function removeTimestampQuery(func) {
return func.call(this)
.then(function (url) {
return modifyQueryString(url, function (parsed) {
parsed.query = _.omit(parsed.query, '_t');
});
});
}
function modifyQueryString(url, func) {
var parsed = parse(url, true);
if (parsed.query === null) {
parsed.query = {};
}
func(parsed);
return format(_.pick(parsed, 'protocol', 'hostname', 'port', 'pathname', 'query', 'hash', 'auth'));
}
function Common(remote) {
this.remote = remote;
if (remote.get.wrapper !== injectTimestampQuery) {
this.remote.get = _.wrap(this.remote.get, injectTimestampQuery);
remote.get.wrapper = injectTimestampQuery;
this.remote.getCurrentUrl = _.wrap(this.remote.getCurrentUrl, removeTimestampQuery);
}
}
var defaultTimeout = config.timeouts.default;
Common.prototype = {
@ -19,22 +53,28 @@ define(function (require) {
navigateToApp: function (appName, testStatusPage) {
var self = this;
var appUrl = getUrl(config.servers.kibana, config.apps[appName]);
self.debug('navigating to ' + appName + ' url: ' + appUrl);
// navUrl includes user:password@ for use with Shield
// appUrl excludes user:password@ to match what getCurrentUrl returns
var navUrl = getUrl(config.servers.kibana, config.apps[appName]);
var appUrl = getUrl.noAuth(config.servers.kibana, config.apps[appName]);
self.debug('navigating to ' + appName + ' url: ' + navUrl);
var doNavigation = function (url) {
return self.tryForTime(defaultTimeout, function () {
// since we're using hash URLs, always reload first to force re-render
self.debug('>>> get ' + url);
self.debug('navigate to: ' + url);
return self.remote.get(url)
.then(function () {
self.debug('<<< get ' + url);
self.debug('returned from get, calling refresh');
return self.remote.refresh();
})
.then(function () {
self.debug('check testStatusPage');
if (testStatusPage !== false) {
self.debug('self.checkForKibanaApp()');
return self.checkForKibanaApp()
.then(function (kibanaLoaded) {
self.debug('kibanaLoaded = ' + kibanaLoaded);
if (!kibanaLoaded) {
var msg = 'Kibana is not loaded, retrying';
self.debug(msg);
@ -51,6 +91,7 @@ define(function (require) {
if (!navSuccessful) {
var msg = 'App failed to load: ' + appName +
' in ' + defaultTimeout + 'ms' +
' appUrl = ' + appUrl +
' currentUrl = ' + currentUrl;
self.debug(msg);
throw new Error(msg);
@ -61,7 +102,7 @@ define(function (require) {
});
};
return doNavigation(appUrl)
return doNavigation(navUrl)
.then(function (currentUrl) {
var lastUrl = currentUrl;
return self.tryForTime(defaultTimeout, function () {

View file

@ -18,6 +18,16 @@ var url = require('url');
* }
* @return {string}
*/
module.exports = function getPage(config, app) {
module.exports = getUrl;
function getUrl(config, app) {
return url.format(_.assign(config, app));
};
getUrl.noAuth = function getUrlNoAuth(config, app) {
config = _.pick(config, function (val, param) {
return param !== 'auth';
});
return getUrl(config, app);
};

View file

@ -3,6 +3,5 @@ require('node_modules/angular/angular');
module.exports = window.angular;
require('node_modules/angular-elastic/elastic');
require('node_modules/angular-route/angular-route');
require('ui/modules').get('kibana', ['ngRoute', 'monospaced.elastic']);
require('ui/modules').get('kibana', ['monospaced.elastic']);