mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge branch 'master' of github.com:elastic/kibana into apps/home
This commit is contained in:
commit
bb77b8a711
34 changed files with 2168 additions and 134 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ target
|
|||
*.log
|
||||
esvm
|
||||
.htpasswd
|
||||
src/server/bin/plugins
|
|
@ -58,6 +58,7 @@
|
|||
"elasticsearch": "^5.0.0",
|
||||
"exports-loader": "^0.6.2",
|
||||
"expose-loader": "^0.7.0",
|
||||
"expiry-js": "^0.1.7",
|
||||
"express": "^4.10.6",
|
||||
"extract-text-webpack-plugin": "^0.8.2",
|
||||
"file-loader": "^0.8.4",
|
||||
|
@ -124,8 +125,7 @@
|
|||
"marked": "^0.3.3",
|
||||
"marked-text-renderer": "^0.1.0",
|
||||
"mocha": "^2.2.5",
|
||||
"nock": "^1.6.0",
|
||||
"nodemon": "^1.3.7",
|
||||
"nock": "^2.7.0",
|
||||
"npm": "^2.11.0",
|
||||
"opn": "^1.0.0",
|
||||
"path-browserify": "0.0.0",
|
||||
|
|
|
@ -17,6 +17,19 @@ ${red(' ERROR ')} ${err}
|
|||
${help(this, ' ')}
|
||||
`
|
||||
);
|
||||
|
||||
process.exit(64);
|
||||
};
|
||||
|
||||
Command.prototype.defaultHelp = function () {
|
||||
console.log(
|
||||
`
|
||||
${help(this, ' ')}
|
||||
|
||||
`
|
||||
);
|
||||
|
||||
process.exit(64);
|
||||
};
|
||||
|
||||
Command.prototype.unknownArgv = function (argv) {
|
||||
|
|
|
@ -10,17 +10,13 @@ let program = new Command('bin/kibana');
|
|||
program
|
||||
.version(pkg.version)
|
||||
.description(
|
||||
'Kibana is an open source (Apache Licensed), browser based analytics ' +
|
||||
'and search dashboard for Elasticsearch.'
|
||||
'Kibana is an open source (Apache Licensed), browser ' +
|
||||
'based analytics and search dashboard for Elasticsearch.'
|
||||
);
|
||||
|
||||
// attach commands
|
||||
var serve = require('./commands/serve')(program);
|
||||
|
||||
// check for no command name
|
||||
if (!argv[2] || argv[2][0] === '-') {
|
||||
argv.splice(2, 0, ['serve']);
|
||||
}
|
||||
require('./commands/serve')(program);
|
||||
require('./commands/plugin/plugin')(program);
|
||||
|
||||
program
|
||||
.command('help <command>')
|
||||
|
@ -37,4 +33,15 @@ program
|
|||
program.error(`unknown command ${cmd}`);
|
||||
});
|
||||
|
||||
// check for no command name
|
||||
var subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//);
|
||||
|
||||
if (!subCommand) {
|
||||
if (_.intersection(argv.slice(2), ['-h', '--help']).length) {
|
||||
program.defaultHelp();
|
||||
} else {
|
||||
argv.splice(2, 0, ['serve']);
|
||||
}
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
|
|
36
src/cli/commands/plugin/npmInstall.js
Normal file
36
src/cli/commands/plugin/npmInstall.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
var Promise = require('bluebird');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
module.exports = function (dest, logger) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
//throw an exception if package.json does not exist
|
||||
try {
|
||||
var packageFile = path.join(dest, 'package.json');
|
||||
fs.statSync(packageFile);
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT')
|
||||
throw e;
|
||||
|
||||
return reject(new Error('Plugin does not contain package.json file'));
|
||||
}
|
||||
|
||||
var cmd = '"' + path.resolve(path.dirname(process.execPath), 'npm').replace(/\\/g, '/') + '" install --production';
|
||||
|
||||
var child = exec(cmd, { cwd: dest });
|
||||
child.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
child.on('exit', function (code, signal) {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('npm install failed with code ' + code));
|
||||
}
|
||||
});
|
||||
|
||||
logger.error(child.stderr);
|
||||
logger.log(child.stdout);
|
||||
});
|
||||
};
|
55
src/cli/commands/plugin/plugin.js
Normal file
55
src/cli/commands/plugin/plugin.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
var settingParser = require('./settingParser');
|
||||
var installer = require('./pluginInstaller');
|
||||
var remover = require('./pluginRemover');
|
||||
var pluginLogger = require('./pluginLogger');
|
||||
|
||||
module.exports = function (program) {
|
||||
function processCommand(command, options) {
|
||||
var settings;
|
||||
try {
|
||||
settings = settingParser(command).parse();
|
||||
} catch (ex) {
|
||||
//The logger has not yet been initialized.
|
||||
console.error(ex.message);
|
||||
process.exit(64);
|
||||
}
|
||||
|
||||
var logger = pluginLogger(settings);
|
||||
|
||||
if (settings.action === 'install') {
|
||||
installer.install(settings, logger);
|
||||
}
|
||||
if (settings.action === 'remove') {
|
||||
remover.remove(settings, logger);
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command('plugin')
|
||||
.option('-i, --install <org>/<plugin>/<version>', 'The plugin to install')
|
||||
.option('-r, --remove <plugin>', 'The plugin to remove')
|
||||
.option('-q, --quiet', 'Disable all process messaging except errors')
|
||||
.option('-s, --silent', 'Disable all process messaging')
|
||||
.option('-u, --url <url>', 'Specify download url')
|
||||
.option('-t, --timeout <duration>', 'Length of time before failing; 0 for never fail', settingParser.parseMilliseconds)
|
||||
.description(
|
||||
'Maintain Plugins',
|
||||
`
|
||||
Common examples:
|
||||
-i username/sample
|
||||
attempts to download the latest version from the following urls:
|
||||
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:
|
||||
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,
|
||||
and installs the plugin found at that url as "sample"
|
||||
`
|
||||
)
|
||||
.action(processCommand);
|
||||
};
|
40
src/cli/commands/plugin/pluginCleaner.js
Normal file
40
src/cli/commands/plugin/pluginCleaner.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
var rimraf = require('rimraf');
|
||||
var fs = require('fs');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
module.exports = function (settings, logger) {
|
||||
|
||||
function cleanPrevious() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
try {
|
||||
fs.statSync(settings.workingPath);
|
||||
|
||||
logger.log('Found previous install attempt. Deleting...');
|
||||
try {
|
||||
rimraf.sync(settings.workingPath);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
return resolve();
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT')
|
||||
return reject(e);
|
||||
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cleanError() {
|
||||
//delete the working directory.
|
||||
//At this point we're bailing, so swallow any errors on delete.
|
||||
try {
|
||||
rimraf.sync(settings.workingPath);
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
return {
|
||||
cleanPrevious: cleanPrevious,
|
||||
cleanError: cleanError
|
||||
};
|
||||
};
|
94
src/cli/commands/plugin/pluginDownloader.js
Normal file
94
src/cli/commands/plugin/pluginDownloader.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
var _ = require('lodash');
|
||||
var zlib = require('zlib');
|
||||
var Promise = require('bluebird');
|
||||
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 download ' + 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'));
|
||||
}
|
||||
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 = tar.Extract({ path: dest, strip: 1 });
|
||||
|
||||
var requestOptions = { url: source };
|
||||
if (timeout !== 0) {
|
||||
requestOptions.timeout = timeout;
|
||||
}
|
||||
|
||||
return wrappedRequest(requestOptions)
|
||||
.then(function (req) {
|
||||
//debugger;
|
||||
var reporter = progressReporter(logger, req);
|
||||
|
||||
req
|
||||
.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) {
|
||||
//debugger;
|
||||
return Promise.try(function () {
|
||||
//debugger;
|
||||
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
|
||||
};
|
||||
};
|
42
src/cli/commands/plugin/pluginInstaller.js
Normal file
42
src/cli/commands/plugin/pluginInstaller.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
var pluginDownloader = require('./pluginDownloader');
|
||||
var pluginCleaner = require('./pluginCleaner');
|
||||
var npmInstall = require('./npmInstall');
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
install: install
|
||||
};
|
||||
|
||||
function install(settings, logger) {
|
||||
logger.log('installing ' + settings.package);
|
||||
|
||||
try {
|
||||
fs.statSync(settings.pluginPath);
|
||||
|
||||
logger.error('Plugin ' + settings.package + ' already exists. Please remove before installing a new version.');
|
||||
process.exit(70);
|
||||
} 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 () {
|
||||
return npmInstall(settings.workingPath, logger);
|
||||
})
|
||||
.then(function (curious) {
|
||||
fs.renameSync(settings.workingPath, settings.pluginPath);
|
||||
logger.log('Plugin installation complete!');
|
||||
})
|
||||
.catch(function (e) {
|
||||
logger.error('Plugin installation was unsuccessful due to error "' + e.message + '"');
|
||||
cleaner.cleanError();
|
||||
process.exit(70);
|
||||
});
|
||||
}
|
44
src/cli/commands/plugin/pluginLogger.js
Normal file
44
src/cli/commands/plugin/pluginLogger.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
module.exports = function (settings) {
|
||||
var previousLineEnded = true;
|
||||
var silent = !!settings.silent;
|
||||
var quiet = !!settings.quiet;
|
||||
|
||||
function log(data, sameLine) {
|
||||
if (silent || quiet) return;
|
||||
|
||||
if (!sameLine && !previousLineEnded) {
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
//if data is a stream, pipe it.
|
||||
if (data.readable) {
|
||||
data.pipe(process.stdout);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(data);
|
||||
if (!sameLine) process.stdout.write('\n');
|
||||
previousLineEnded = !sameLine;
|
||||
}
|
||||
|
||||
function error(data) {
|
||||
if (silent) return;
|
||||
|
||||
if (!previousLineEnded) {
|
||||
process.stderr.write('\n');
|
||||
}
|
||||
|
||||
//if data is a stream, pipe it.
|
||||
if (data.readable) {
|
||||
data.pipe(process.stderr);
|
||||
return;
|
||||
}
|
||||
process.stderr.write(data + '\n');
|
||||
previousLineEnded = true;
|
||||
}
|
||||
|
||||
return {
|
||||
log: log,
|
||||
error: error
|
||||
};
|
||||
};
|
26
src/cli/commands/plugin/pluginRemover.js
Normal file
26
src/cli/commands/plugin/pluginRemover.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
var fs = require('fs');
|
||||
var rimraf = require('rimraf');
|
||||
|
||||
module.exports = {
|
||||
remove: remove
|
||||
};
|
||||
|
||||
function remove(settings, logger) {
|
||||
try {
|
||||
try {
|
||||
fs.statSync(settings.pluginPath);
|
||||
}
|
||||
catch (e) {
|
||||
logger.log('Plugin ' + settings.package + ' does not exist.');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Removing ' + settings.package + '...');
|
||||
|
||||
rimraf.sync(settings.pluginPath);
|
||||
} catch (err) {
|
||||
var message = 'Unable to remove plugin "' + settings.package + '" because of error: "' + err.message + '"';
|
||||
logger.error(message);
|
||||
process.exit(74);
|
||||
}
|
||||
}
|
71
src/cli/commands/plugin/progressReporter.js
Normal file
71
src/cli/commands/plugin/progressReporter.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
var Promise = require('bluebird');
|
||||
|
||||
/*
|
||||
Responsible for reporting the progress of the file request stream
|
||||
*/
|
||||
module.exports = function (logger, request) {
|
||||
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;
|
||||
request.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('Download Complete.');
|
||||
_resolve();
|
||||
}
|
||||
|
||||
return {
|
||||
promise: promise,
|
||||
handleResponse: handleResponse,
|
||||
handleError: handleError,
|
||||
handleData: handleData,
|
||||
handleEnd: handleEnd,
|
||||
hasError: function () { return hasError; }
|
||||
};
|
||||
};
|
108
src/cli/commands/plugin/settingParser.js
Normal file
108
src/cli/commands/plugin/settingParser.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
var path = require('path');
|
||||
var expiry = require('expiry-js');
|
||||
|
||||
module.exports = function (options) {
|
||||
function parseMilliseconds(val) {
|
||||
var result;
|
||||
|
||||
try {
|
||||
var timeVal = expiry(val);
|
||||
result = timeVal.asMilliseconds();
|
||||
} catch (ex) {
|
||||
result = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateDownloadUrl(settings) {
|
||||
var version = (settings.version) || 'latest';
|
||||
var 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 = {
|
||||
timeout: 0,
|
||||
silent: false,
|
||||
quiet: false,
|
||||
urls: []
|
||||
};
|
||||
|
||||
settings.workingPath = path.resolve(__dirname, '..', 'plugins', '.plugin.installing');
|
||||
|
||||
if (options.timeout) {
|
||||
settings.timeout = options.timeout;
|
||||
}
|
||||
|
||||
if (options.parent && options.parent.quiet) {
|
||||
settings.quiet = options.parent.quiet;
|
||||
}
|
||||
|
||||
if (options.silent) {
|
||||
settings.silent = options.silent;
|
||||
}
|
||||
|
||||
if (options.url) {
|
||||
settings.urls.push(options.url);
|
||||
}
|
||||
|
||||
if (options.install) {
|
||||
settings.action = 'install';
|
||||
parts = options.install.split('/');
|
||||
|
||||
if (options.url) {
|
||||
if (parts.length !== 1) {
|
||||
throw new Error('Invalid install option. When providing a url, please use the format <plugin>.');
|
||||
}
|
||||
|
||||
settings.package = parts.shift();
|
||||
} else {
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
throw new Error('Invalid install option. Please use the format <org>/<plugin>/<version>.');
|
||||
}
|
||||
|
||||
settings.organization = parts.shift();
|
||||
settings.package = parts.shift();
|
||||
settings.version = parts.shift();
|
||||
|
||||
settings.urls.push(generateDownloadUrl(settings));
|
||||
settings.urls.push(generateGithubUrl(settings));
|
||||
}
|
||||
}
|
||||
|
||||
if (options.remove) {
|
||||
settings.action = 'remove';
|
||||
parts = options.remove.split('/');
|
||||
|
||||
if (parts.length !== 1) {
|
||||
throw new Error('Invalid remove option. Please use the format <plugin>.');
|
||||
}
|
||||
settings.package = parts.shift();
|
||||
}
|
||||
|
||||
if (!settings.action || (options.install && options.remove)) {
|
||||
throw new Error('Please specify either --install or --remove.');
|
||||
}
|
||||
|
||||
if (settings.package) {
|
||||
settings.pluginPath = path.resolve(__dirname, '..', 'plugins', settings.package);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
return {
|
||||
parse: parse,
|
||||
parseMilliseconds: parseMilliseconds
|
||||
};
|
||||
};
|
|
@ -14,7 +14,7 @@ module.exports = function (command, spaces) {
|
|||
let desc = !command.description() ? '' : command.description();
|
||||
let cmdDef = !defCmd ? '' : `=${defCmd._name}`;
|
||||
|
||||
return _.trim(
|
||||
return (
|
||||
`
|
||||
Usage: ${command._name} [command${cmdDef}] [options]
|
||||
|
||||
|
@ -25,7 +25,7 @@ ${indent(commandsSummary(command), 2)}
|
|||
|
||||
${cmdHelp(defCmd)}
|
||||
`
|
||||
).replace(/^/gm, spaces || '');
|
||||
).trim().replace(/^/gm, spaces || '');
|
||||
};
|
||||
|
||||
function indent(str, n) {
|
||||
|
@ -58,13 +58,15 @@ function commandsSummary(program) {
|
|||
|
||||
function cmdHelp(cmd) {
|
||||
if (!cmd) return '';
|
||||
return _.trim(
|
||||
return (
|
||||
|
||||
`
|
||||
"${cmd._name}" Options:
|
||||
|
||||
${indent(cmd.optionHelp(), 2)}
|
||||
`
|
||||
);
|
||||
|
||||
).trim();
|
||||
|
||||
}
|
||||
|
||||
|
|
9
src/server/lib/commanderExtensions.js
Normal file
9
src/server/lib/commanderExtensions.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
module.exports = function (program) {
|
||||
function isCommand(val) {
|
||||
return typeof val === 'object' && val._name;
|
||||
}
|
||||
|
||||
program.isCommandSpecified = function () {
|
||||
return program.args.some(isCommand);
|
||||
};
|
||||
};
|
|
@ -12,7 +12,7 @@ define(function (require) {
|
|||
group: 'none',
|
||||
name: 'orderAgg',
|
||||
title: 'Order Agg',
|
||||
aggFilter: ['!percentiles', '!std_dev']
|
||||
aggFilter: ['!percentiles', '!median', '!std_dev']
|
||||
}
|
||||
])).all[0];
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ define(function (require) {
|
|||
Private(require('components/agg_types/metrics/count')),
|
||||
Private(require('components/agg_types/metrics/avg')),
|
||||
Private(require('components/agg_types/metrics/sum')),
|
||||
Private(require('components/agg_types/metrics/median')),
|
||||
Private(require('components/agg_types/metrics/min')),
|
||||
Private(require('components/agg_types/metrics/max')),
|
||||
Private(require('components/agg_types/metrics/std_deviation')),
|
||||
|
|
29
src/ui/components/agg_types/metrics/median.js
Normal file
29
src/ui/components/agg_types/metrics/median.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
define(function (require) {
|
||||
return function AggTypeMetricMaxProvider(Private) {
|
||||
var _ = require('lodash');
|
||||
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
|
||||
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
|
||||
var percentiles = Private(require('components/agg_types/metrics/percentiles'));
|
||||
|
||||
return new MetricAggType({
|
||||
name: 'median',
|
||||
dslName: 'percentiles',
|
||||
title: 'Median',
|
||||
makeLabel: function (aggConfig) {
|
||||
return 'Median ' + aggConfig.params.field.displayName;
|
||||
},
|
||||
params: [
|
||||
{
|
||||
name: 'field',
|
||||
filterFieldTypes: 'number'
|
||||
},
|
||||
{
|
||||
name: 'percents',
|
||||
default: [50]
|
||||
}
|
||||
],
|
||||
getResponseAggs: percentiles.getResponseAggs,
|
||||
getValue: percentiles.getValue
|
||||
});
|
||||
};
|
||||
});
|
|
@ -2,22 +2,28 @@ define(function (require) {
|
|||
var module = require('modules').get('kibana');
|
||||
var _ = require('lodash');
|
||||
var rison = require('utils/rison');
|
||||
var keymap = require('utils/key_map');
|
||||
|
||||
module.directive('savedObjectFinder', function (savedSearches, savedVisualizations, savedDashboards, $location, kbnUrl) {
|
||||
|
||||
var vars = {
|
||||
var types = {
|
||||
searches: {
|
||||
service: savedSearches,
|
||||
name: 'searches',
|
||||
noun: 'Saved Search',
|
||||
nouns: 'Saved Searches'
|
||||
nouns: 'searches'
|
||||
},
|
||||
visualizations: {
|
||||
service: savedVisualizations,
|
||||
noun: 'Visualization'
|
||||
name: 'visualizations',
|
||||
noun: 'Visualization',
|
||||
nouns: 'visualizations'
|
||||
},
|
||||
dashboards: {
|
||||
service: savedDashboards,
|
||||
noun: 'Dashboard'
|
||||
name: 'dashboards',
|
||||
noun: 'Dashboard',
|
||||
nouns: 'dashboards'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -32,14 +38,17 @@ define(function (require) {
|
|||
userOnChoose: '=?onChoose'
|
||||
},
|
||||
template: require('partials/saved_object_finder.html'),
|
||||
link: function ($scope, $el) {
|
||||
controllerAs: 'finder',
|
||||
controller: function ($scope, $element, $timeout) {
|
||||
var self = this;
|
||||
|
||||
// the text input element
|
||||
var $input = $el.find('input[ng-model=filter]');
|
||||
var $input = $element.find('input[ng-model=filter]');
|
||||
|
||||
// the list that will hold the suggestions
|
||||
var $list = $el.find('.finder-options');
|
||||
var $list = $element.find('ul');
|
||||
|
||||
// the current filter string, used to check that retured results are still useful
|
||||
// the current filter string, used to check that returned results are still useful
|
||||
var currentFilter = $scope.filter;
|
||||
|
||||
// the most recently entered search/filter
|
||||
|
@ -48,18 +57,19 @@ define(function (require) {
|
|||
// the service we will use to find records
|
||||
var service;
|
||||
|
||||
// the currently selected jQuery element
|
||||
var $selected = null;
|
||||
|
||||
// the list of hits, used to render display
|
||||
$scope.hits = [];
|
||||
self.hits = [];
|
||||
|
||||
self.objectType = types[$scope.type];
|
||||
|
||||
filterResults();
|
||||
|
||||
/**
|
||||
* Passed the hit objects and will determine if the
|
||||
* hit should have a url in the UI, returns it if so
|
||||
* @return {string|null} - the url or nothing
|
||||
*/
|
||||
$scope.makeUrl = function (hit) {
|
||||
self.makeUrl = function (hit) {
|
||||
if ($scope.userMakeUrl) {
|
||||
return $scope.userMakeUrl(hit);
|
||||
}
|
||||
|
@ -67,21 +77,27 @@ define(function (require) {
|
|||
if (!$scope.userOnChoose) {
|
||||
return hit.url;
|
||||
}
|
||||
|
||||
return '#';
|
||||
};
|
||||
|
||||
self.preventClick = function ($event) {
|
||||
$event.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when a hit object is clicked, can override the
|
||||
* url behavior if necessary.
|
||||
*/
|
||||
$scope.onChoose = function (hit, $event) {
|
||||
self.onChoose = function (hit, $event) {
|
||||
if ($scope.userOnChoose) {
|
||||
$scope.userOnChoose(hit, $event);
|
||||
}
|
||||
|
||||
if ($event.isDefaultPrevented()) return;
|
||||
|
||||
var url = $scope.makeUrl(hit);
|
||||
if (!url || url.charAt(0) !== '#') return;
|
||||
var url = self.makeUrl(hit);
|
||||
if (!url || url === '#' || url.charAt(0) !== '#') return;
|
||||
|
||||
$event.preventDefault();
|
||||
|
||||
|
@ -89,14 +105,6 @@ define(function (require) {
|
|||
kbnUrl.change(url.substr(1));
|
||||
};
|
||||
|
||||
$scope.$watch('type', function (type) {
|
||||
type = vars[type];
|
||||
service = type.service;
|
||||
$scope.noun = type.noun;
|
||||
$scope.nouns = type.nouns || type.noun + 's';
|
||||
filterResults();
|
||||
});
|
||||
|
||||
$scope.$watch('filter', function (newFilter) {
|
||||
// ensure that the currentFilter changes from undefined to ''
|
||||
// which triggers
|
||||
|
@ -104,100 +112,124 @@ define(function (require) {
|
|||
filterResults();
|
||||
});
|
||||
|
||||
$scope.selectedItem = false;
|
||||
$input.on('keydown', (function () {
|
||||
var enter = 13;
|
||||
var up = 38;
|
||||
var down = 40;
|
||||
var left = 37;
|
||||
var right = 39;
|
||||
var esc = 27;
|
||||
//manages the state of the keyboard selector
|
||||
self.selector = {
|
||||
enabled: false,
|
||||
index: -1
|
||||
};
|
||||
|
||||
var scrollIntoView = function ($el, snapTop) {
|
||||
var el = $el[0];
|
||||
//key handler for the filter text box
|
||||
self.filterKeyDown = function ($event) {
|
||||
if (keymap[$event.keyCode] !== 'tab') return;
|
||||
|
||||
if (!el) return;
|
||||
if (self.hits.length === 0) return;
|
||||
|
||||
if ('scrollIntoViewIfNeeded' in el) {
|
||||
el.scrollIntoViewIfNeeded(snapTop);
|
||||
} else if ('scrollIntoView' in el) {
|
||||
el.scrollIntoView(snapTop);
|
||||
}
|
||||
};
|
||||
self.selector.index = 0;
|
||||
self.selector.enabled = true;
|
||||
|
||||
return function (event) {
|
||||
var $next;
|
||||
var goingUp;
|
||||
selectTopHit();
|
||||
|
||||
switch (event.keyCode) {
|
||||
case enter:
|
||||
if (!$selected) return;
|
||||
$event.preventDefault();
|
||||
};
|
||||
|
||||
// get the index of the selected element
|
||||
var i = $list.find('li').index($selected);
|
||||
//key handler for the list items
|
||||
self.hitKeyDown = function ($event, page, paginate) {
|
||||
switch (keymap[$event.keyCode]) {
|
||||
case 'tab':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
// get the related hit item
|
||||
var hit = $scope.hits[i];
|
||||
self.selector.index = -1;
|
||||
self.selector.enabled = false;
|
||||
|
||||
if (!hit) return;
|
||||
//if the user types shift-tab return to the textbox
|
||||
//if the user types tab, set the focus to the currently selected hit.
|
||||
if ($event.shiftKey) {
|
||||
$input.focus();
|
||||
} else {
|
||||
$list.find('li.active a').focus();
|
||||
}
|
||||
|
||||
// check if there is a url for this hit
|
||||
var url = $scope.makeUrl(hit);
|
||||
|
||||
if (url) window.location = url;
|
||||
$scope.onChoose(hit);
|
||||
|
||||
return;
|
||||
case up:
|
||||
$next = $selected ? $selected.prev() : $list.find('li:last-child');
|
||||
goingUp = false;
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case down:
|
||||
$next = $selected ? $selected.next() : $list.find('li:first-child');
|
||||
goingUp = true;
|
||||
case 'down':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
if (self.selector.index + 1 < page.length) {
|
||||
self.selector.index += 1;
|
||||
}
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case esc:
|
||||
scrollIntoView($list.find('li:first-child'));
|
||||
$next = null;
|
||||
case 'up':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
if (self.selector.index > 0) {
|
||||
self.selector.index -= 1;
|
||||
}
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case 'right':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
if (page.number < page.count) {
|
||||
paginate.goToPage(page.number + 1);
|
||||
self.selector.index = 0;
|
||||
selectTopHit();
|
||||
}
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case 'left':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
if (page.number > 1) {
|
||||
paginate.goToPage(page.number - 1);
|
||||
self.selector.index = 0;
|
||||
selectTopHit();
|
||||
}
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case 'escape':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
$input.focus();
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case 'enter':
|
||||
if (!self.selector.enabled) break;
|
||||
|
||||
var hitIndex = ((page.number - 1) * paginate.perPage) + self.selector.index;
|
||||
var hit = self.hits[hitIndex];
|
||||
if (!hit) break;
|
||||
|
||||
self.onChoose(hit, $event);
|
||||
$event.preventDefault();
|
||||
break;
|
||||
case 'shift':
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
$input.focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if ($next && $next.length === 0) {
|
||||
// we are at one of the ends
|
||||
return;
|
||||
}
|
||||
self.hitBlur = function ($event) {
|
||||
self.selector.index = -1;
|
||||
self.selector.enabled = false;
|
||||
};
|
||||
|
||||
if ($selected && $next && $next.eq($selected).length) {
|
||||
// the selections are the same, bail
|
||||
return;
|
||||
}
|
||||
|
||||
if ($selected) {
|
||||
$selected.removeClass('active');
|
||||
$selected = null;
|
||||
}
|
||||
|
||||
if ($next) {
|
||||
// remove selection stuff from $selected
|
||||
$next.addClass('active');
|
||||
scrollIntoView($next, goingUp);
|
||||
$selected = $next;
|
||||
}
|
||||
};
|
||||
}()));
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$input.off('keydown');
|
||||
});
|
||||
|
||||
$scope.manageObject = function (type) {
|
||||
self.manageObjects = function (type) {
|
||||
$location.url('/settings/objects?_a=' + rison.encode({tab: type}));
|
||||
};
|
||||
|
||||
function selectTopHit() {
|
||||
setTimeout(function () {
|
||||
//triggering a focus event kicks off a new angular digest cycle.
|
||||
$list.find('a:first').focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function filterResults() {
|
||||
if (!service) return;
|
||||
if (!self.objectType) return;
|
||||
if (!self.objectType.service) return;
|
||||
|
||||
// track the filter that we use for this search,
|
||||
// but ensure that we don't search for the same
|
||||
|
@ -207,18 +239,29 @@ define(function (require) {
|
|||
if (prevSearch === filter) return;
|
||||
|
||||
prevSearch = filter;
|
||||
service.find(filter)
|
||||
self.objectType.service.find(filter)
|
||||
.then(function (hits) {
|
||||
// ensure that we don't display old results
|
||||
// as we can't really cancel requests
|
||||
if (currentFilter === filter) {
|
||||
$scope.hitCount = hits.total;
|
||||
$scope.hits = hits.hits;
|
||||
$selected = null;
|
||||
self.hits = _.sortBy(hits.hits, 'title');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollIntoView($element, snapTop) {
|
||||
var el = $element[0];
|
||||
|
||||
if (!el) return;
|
||||
|
||||
if ('scrollIntoViewIfNeeded' in el) {
|
||||
el.scrollIntoViewIfNeeded(snapTop);
|
||||
} else if ('scrollIntoView' in el) {
|
||||
el.scrollIntoView(snapTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,30 +1,47 @@
|
|||
<form role="form">
|
||||
<div class="form-group finder-form">
|
||||
<div class="finder-form-options">
|
||||
<a class="small" ng-click="manageObject(type)">manage {{type}}</a>
|
||||
<a class="small" ng-click="finder.manageObjects(finder.objectType.name)">manage {{finder.objectType.name}}</a>
|
||||
</div>
|
||||
<div class="clearfix visible-xs-block"></div>
|
||||
<input
|
||||
input-focus
|
||||
ng-model="filter"
|
||||
ng-attr-placeholder="{{noun}} Filter"
|
||||
ng-attr-placeholder="{{finder.objectType.noun}} Filter"
|
||||
ng-keydown="finder.filterKeyDown($event)"
|
||||
class="form-control"
|
||||
name="filter"
|
||||
type="text">
|
||||
<span class="finder-hit-count"><strong>{{hitCount}}</strong> {{type}}</span>
|
||||
type="text" />
|
||||
|
||||
<span class="finder-hit-count"><strong>{{hitCount}}</strong> {{finder.objectType.name}}</span>
|
||||
</div>
|
||||
</form>
|
||||
<paginate list="hits | orderBy:'title'" per-page="5">
|
||||
<ul class="list-group list-group-menu">
|
||||
<a class="list-group-item list-group-menu-item"
|
||||
<paginate list="finder.hits" per-page="5">
|
||||
<ul
|
||||
class="list-group list-group-menu"
|
||||
ng-class="{'select-mode': finder.selector.enabled}">
|
||||
|
||||
<li
|
||||
class="list-group-item list-group-menu-item"
|
||||
ng-class="{'active': finder.selector.index === $index && finder.selector.enabled}"
|
||||
ng-repeat="hit in page"
|
||||
ng-href="{{ makeUrl(hit) }}"
|
||||
ng-click="onChoose(hit, $event)">
|
||||
<li>
|
||||
ng-keydown="finder.hitKeyDown($event, page, paginate)"
|
||||
ng-click="finder.onChoose(hit, $event)">
|
||||
|
||||
<a
|
||||
ng-href="{{finder.makeUrl(hit)}}"
|
||||
ng-blur="finder.hitBlur($event)"
|
||||
ng-click="finder.preventClick($event)">
|
||||
|
||||
<i aria-hidden="true" class="fa" ng-if="hit.icon" ng-class="hit.icon"></i> {{hit.title}}
|
||||
<p ng-if="hit.description" ng-bind="hit.description"></p>
|
||||
</li>
|
||||
</a>
|
||||
<p ng-if="hits.length === 0" ng-bind="'No matching ' + nouns + ' found.'"></p>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="list-group-item list-group-no-results"
|
||||
ng-if="finder.hits.length === 0">
|
||||
|
||||
<p ng-bind="'No matching ' + finder.objectType.nouns + ' found.'"></p>
|
||||
</li>
|
||||
</ul>
|
||||
</paginate>
|
||||
</paginate>
|
|
@ -276,6 +276,10 @@ saved-object-finder {
|
|||
&:first-child {
|
||||
.border-top-radius(0) !important;
|
||||
}
|
||||
|
||||
&.list-group-no-results p {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.finder-form {
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
@import (reference) "~ui-styles/theme";
|
||||
|
||||
.list-group-menu {
|
||||
|
||||
&.select-mode a{
|
||||
outline: none;
|
||||
color: @link-color;
|
||||
}
|
||||
|
||||
.list-group-menu-item {
|
||||
list-style: none;
|
||||
color: @link-color;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
background-color: @well-bg;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @well-bg;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
color: @link-color;
|
||||
|
|
69
test/unit/server/bin/plugin/npmInstall.js
Normal file
69
test/unit/server/bin/plugin/npmInstall.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
var root = require('requirefrom')('');
|
||||
var expect = require('expect.js');
|
||||
var nock = require('nock');
|
||||
var glob = require('glob');
|
||||
var rimraf = require('rimraf');
|
||||
var fs = require('fs');
|
||||
var join = require('path').join;
|
||||
var sinon = require('sinon');
|
||||
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
|
||||
var npmInstall = root('src/server/bin/plugin/npmInstall');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('npmInstall', function () {
|
||||
|
||||
var logger;
|
||||
var testWorkingPath = join(__dirname, '.test.data');
|
||||
var statSyncStub;
|
||||
|
||||
beforeEach(function () {
|
||||
statSyncStub = undefined;
|
||||
logger = pluginLogger(false);
|
||||
rimraf.sync(testWorkingPath);
|
||||
sinon.stub(logger, 'log');
|
||||
sinon.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
logger.log.restore();
|
||||
logger.error.restore();
|
||||
rimraf.sync(testWorkingPath);
|
||||
if (statSyncStub) statSyncStub.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if there is no package.json file in the archive', function () {
|
||||
fs.mkdirSync(testWorkingPath);
|
||||
|
||||
var errorStub = sinon.stub();
|
||||
return npmInstall(testWorkingPath, logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(errorStub.called).to.be(true);
|
||||
expect(errorStub.lastCall.args[0].message).to.match(/package.json/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should rethrow any errors other than "ENOENT" from fs.statSync', function () {
|
||||
fs.mkdirSync(testWorkingPath);
|
||||
|
||||
statSyncStub = sinon.stub(fs, 'statSync', function () {
|
||||
throw new Error('This is unexpected.');
|
||||
});
|
||||
|
||||
var errorStub = sinon.stub();
|
||||
return npmInstall(testWorkingPath, logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(errorStub.called).to.be(true);
|
||||
expect(errorStub.lastCall.args[0].message).to.match(/This is unexpected./);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
80
test/unit/server/bin/plugin/plugin.js
Normal file
80
test/unit/server/bin/plugin/plugin.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
var root = require('requirefrom')('');
|
||||
var plugin = root('src/server/bin/plugin/plugin');
|
||||
var expect = require('expect.js');
|
||||
var sinon = require('sinon');
|
||||
var installer = root('src/server/bin/plugin/pluginInstaller');
|
||||
var remover = root('src/server/bin/plugin/pluginRemover');
|
||||
var settingParser = root('src/server/bin/plugin/settingParser');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('commander options', function () {
|
||||
|
||||
var program = {
|
||||
command: function () { return program; },
|
||||
description: function () { return program; },
|
||||
option: function () { return program; },
|
||||
action: function () { return program; }
|
||||
};
|
||||
|
||||
it('should define the command', function () {
|
||||
sinon.spy(program, 'command');
|
||||
|
||||
plugin(program);
|
||||
expect(program.command.calledWith('plugin')).to.be(true);
|
||||
|
||||
program.command.restore();
|
||||
});
|
||||
|
||||
it('should define the description', function () {
|
||||
sinon.spy(program, 'description');
|
||||
|
||||
plugin(program);
|
||||
expect(program.description.calledWith('Maintain Plugins')).to.be(true);
|
||||
|
||||
program.description.restore();
|
||||
});
|
||||
|
||||
it('should define the command line options', function () {
|
||||
var spy = sinon.spy(program, 'option');
|
||||
|
||||
var options = [
|
||||
/-i/,
|
||||
/-r/,
|
||||
/-s/,
|
||||
/-u/,
|
||||
/-t/
|
||||
];
|
||||
|
||||
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];
|
||||
if (call.args[0].match(option)) {
|
||||
options.splice(o, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(options).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should call the action function', function () {
|
||||
sinon.spy(program, 'action');
|
||||
|
||||
plugin(program);
|
||||
expect(program.action.calledOnce).to.be(true);
|
||||
|
||||
program.action.restore();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
147
test/unit/server/bin/plugin/pluginCleaner.js
Normal file
147
test/unit/server/bin/plugin/pluginCleaner.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
var root = require('requirefrom')('');
|
||||
var expect = require('expect.js');
|
||||
var sinon = require('sinon');
|
||||
var fs = require('fs');
|
||||
var rimraf = require('rimraf');
|
||||
var pluginCleaner = root('src/server/bin/plugin/pluginCleaner');
|
||||
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('pluginCleaner', function () {
|
||||
|
||||
var settings = {
|
||||
workingPath: 'dummy'
|
||||
};
|
||||
|
||||
describe('cleanPrevious', function () {
|
||||
|
||||
var cleaner;
|
||||
var errorStub;
|
||||
var logger;
|
||||
var progress;
|
||||
var request;
|
||||
|
||||
beforeEach(function () {
|
||||
errorStub = sinon.stub();
|
||||
logger = pluginLogger(false);
|
||||
cleaner = pluginCleaner(settings, logger);
|
||||
sinon.stub(logger, 'log');
|
||||
sinon.stub(logger, 'error');
|
||||
request = {
|
||||
abort: sinon.stub(),
|
||||
emit: sinon.stub()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
logger.log.restore();
|
||||
logger.error.restore();
|
||||
fs.statSync.restore();
|
||||
rimraf.sync.restore();
|
||||
});
|
||||
|
||||
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');
|
||||
error.code = 'ENOENT';
|
||||
throw error;
|
||||
});
|
||||
|
||||
return cleaner.cleanPrevious(logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(errorStub.called).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
throw error;
|
||||
});
|
||||
|
||||
var errorStub = sinon.stub();
|
||||
return cleaner.cleanPrevious(logger)
|
||||
.catch(errorStub)
|
||||
.then(function () {
|
||||
expect(errorStub.called).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should log a message if there was a working directory', function () {
|
||||
sinon.stub(rimraf, 'sync');
|
||||
sinon.stub(fs, 'statSync');
|
||||
|
||||
return cleaner.cleanPrevious(logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(logger.log.calledWith('Found previous install attempt. Deleting...')).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should rethrow any exception from rimraf.sync', function () {
|
||||
sinon.stub(fs, 'statSync');
|
||||
sinon.stub(rimraf, 'sync', function () {
|
||||
throw new Error('I am an error thrown by rimraf');
|
||||
});
|
||||
|
||||
var errorStub = sinon.stub();
|
||||
return cleaner.cleanPrevious(logger)
|
||||
.catch(errorStub)
|
||||
.then(function () {
|
||||
expect(errorStub.called).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve if the working path is deleted', function () {
|
||||
sinon.stub(rimraf, 'sync');
|
||||
sinon.stub(fs, 'statSync');
|
||||
|
||||
return cleaner.cleanPrevious(logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(errorStub.called).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('cleanError', function () {
|
||||
var cleaner;
|
||||
var logger;
|
||||
beforeEach(function () {
|
||||
logger = pluginLogger(false);
|
||||
cleaner = pluginCleaner(settings, logger);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
rimraf.sync.restore();
|
||||
});
|
||||
|
||||
it('should attempt to delete the working directory', function () {
|
||||
sinon.stub(rimraf, 'sync');
|
||||
|
||||
cleaner.cleanError();
|
||||
expect(rimraf.sync.calledWith(settings.workingPath)).to.be(true);
|
||||
});
|
||||
|
||||
it('should swallow any errors thrown by rimraf.sync', function () {
|
||||
sinon.stub(rimraf, 'sync', function () {
|
||||
throw new Error('Something bad happened.');
|
||||
});
|
||||
|
||||
expect(cleaner.cleanError).withArgs(settings).to.not.throwError();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
249
test/unit/server/bin/plugin/pluginDownloader.js
Normal file
249
test/unit/server/bin/plugin/pluginDownloader.js
Normal file
|
@ -0,0 +1,249 @@
|
|||
var root = require('requirefrom')('');
|
||||
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').join;
|
||||
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
|
||||
var pluginDownloader = root('src/server/bin/plugin/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('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('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('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('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('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([]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
74
test/unit/server/bin/plugin/pluginInstaller.js
Normal file
74
test/unit/server/bin/plugin/pluginInstaller.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
var root = require('requirefrom')('');
|
||||
var expect = require('expect.js');
|
||||
var sinon = require('sinon');
|
||||
var nock = require('nock');
|
||||
var glob = require('glob');
|
||||
var rimraf = require('rimraf');
|
||||
var fs = require('fs');
|
||||
var join = require('path').join;
|
||||
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
|
||||
var pluginInstaller = root('src/server/bin/plugin/pluginInstaller');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('pluginInstaller', function () {
|
||||
|
||||
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);
|
||||
sinon.stub(logger, 'log');
|
||||
sinon.stub(logger, 'error');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (processExitStub) processExitStub.restore();
|
||||
if (statSyncStub) statSyncStub.restore();
|
||||
logger.log.restore();
|
||||
logger.error.restore();
|
||||
rimraf.sync(testWorkingPath);
|
||||
});
|
||||
|
||||
it('should throw an error if the workingPath already exists.', function () {
|
||||
processExitStub = sinon.stub(process, 'exit');
|
||||
fs.mkdirSync(testWorkingPath);
|
||||
|
||||
var settings = {
|
||||
pluginPath: testWorkingPath
|
||||
};
|
||||
|
||||
var errorStub = sinon.stub();
|
||||
return pluginInstaller.install(settings, logger)
|
||||
.catch(errorStub)
|
||||
.then(function (data) {
|
||||
expect(logger.error.firstCall.args[0]).to.match(/already exists/);
|
||||
expect(process.exit.called).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
128
test/unit/server/bin/plugin/pluginLogger.js
Normal file
128
test/unit/server/bin/plugin/pluginLogger.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
var root = require('requirefrom')('');
|
||||
var pluginLogger = root('src/server/bin/plugin/pluginLogger');
|
||||
var expect = require('expect.js');
|
||||
var sinon = require('sinon');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('logger', function () {
|
||||
|
||||
var logger;
|
||||
|
||||
describe('logger.log', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.spy(process.stdout, 'write');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
process.stdout.write.restore();
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
logger.log(message);
|
||||
|
||||
var 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++) {
|
||||
logger.log('.', true);
|
||||
}
|
||||
logger.log('Done!');
|
||||
|
||||
expect(process.stdout.write.callCount).to.be(13);
|
||||
expect(process.stdout.write.getCall(0).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(1).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(2).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(3).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(4).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(5).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(6).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(7).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(8).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(9).args[0]).to.be('.');
|
||||
expect(process.stdout.write.getCall(10).args[0]).to.be('\n');
|
||||
expect(process.stdout.write.getCall(11).args[0]).to.be('Done!');
|
||||
expect(process.stdout.write.getCall(12).args[0]).to.be('\n');
|
||||
});
|
||||
|
||||
it('should not log any messages when quiet is set', function () {
|
||||
logger = pluginLogger({ silent: false, quiet: true });
|
||||
|
||||
var message = 'this is my message';
|
||||
logger.log(message);
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
logger.log('.', true);
|
||||
}
|
||||
logger.log('Done!');
|
||||
|
||||
expect(process.stdout.write.callCount).to.be(0);
|
||||
});
|
||||
|
||||
it('should not log any messages when silent is set', function () {
|
||||
logger = pluginLogger({ silent: true, quiet: false });
|
||||
|
||||
var message = 'this is my message';
|
||||
logger.log(message);
|
||||
|
||||
for (var i = 0; i < 10; i++) {
|
||||
logger.log('.', true);
|
||||
}
|
||||
logger.log('Done!');
|
||||
|
||||
expect(process.stdout.write.callCount).to.be(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('logger.error', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.spy(process.stderr, 'write');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
process.stderr.write.restore();
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
logger.error(message);
|
||||
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
logger.error(message);
|
||||
expect(process.stderr.write.calledWith(message + '\n')).to.be(true);
|
||||
});
|
||||
|
||||
it('should not log any error messages when silent is set', function () {
|
||||
logger = pluginLogger({ silent: true, quiet: false });
|
||||
var message = 'this is my error';
|
||||
|
||||
logger.error(message);
|
||||
expect(process.stderr.write.callCount).to.be(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
302
test/unit/server/bin/plugin/progressReporter.js
Normal file
302
test/unit/server/bin/plugin/progressReporter.js
Normal file
|
@ -0,0 +1,302 @@
|
|||
var root = require('requirefrom')('');
|
||||
var expect = require('expect.js');
|
||||
var sinon = require('sinon');
|
||||
var progressReporter = root('src/server/bin/plugin/progressReporter');
|
||||
var pluginLogger = root('src/server/bin/plugin/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!/);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
BIN
test/unit/server/bin/plugin/replies/corrupt.tar.gz
Normal file
BIN
test/unit/server/bin/plugin/replies/corrupt.tar.gz
Normal file
Binary file not shown.
13
test/unit/server/bin/plugin/replies/package.json
Normal file
13
test/unit/server/bin/plugin/replies/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"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"
|
||||
}
|
BIN
test/unit/server/bin/plugin/replies/plugin-no-package.gz
Normal file
BIN
test/unit/server/bin/plugin/replies/plugin-no-package.gz
Normal file
Binary file not shown.
BIN
test/unit/server/bin/plugin/replies/test-plugin-master.tar.gz
Normal file
BIN
test/unit/server/bin/plugin/replies/test-plugin-master.tar.gz
Normal file
Binary file not shown.
323
test/unit/server/bin/plugin/settingParser.js
Normal file
323
test/unit/server/bin/plugin/settingParser.js
Normal file
|
@ -0,0 +1,323 @@
|
|||
var root = require('requirefrom')('');
|
||||
var settingParser = root('src/server/bin/plugin/settingParser');
|
||||
var path = require('path');
|
||||
var expect = require('expect.js');
|
||||
|
||||
describe('kibana cli', function () {
|
||||
|
||||
describe('plugin installer', function () {
|
||||
|
||||
describe('command line option parsing', function () {
|
||||
|
||||
describe('parseMilliseconds function', function () {
|
||||
|
||||
var parser = settingParser();
|
||||
|
||||
it('should return 0 for an empty string', function () {
|
||||
var value = '';
|
||||
|
||||
var result = parser.parseMilliseconds(value);
|
||||
|
||||
expect(result).to.be(0);
|
||||
});
|
||||
|
||||
it('should return 0 for a number with an invalid unit of measure', function () {
|
||||
var result = parser.parseMilliseconds('1gigablasts');
|
||||
expect(result).to.be(0);
|
||||
});
|
||||
|
||||
it('should assume a number with no unit of measure is specified as milliseconds', function () {
|
||||
var result = parser.parseMilliseconds(1);
|
||||
expect(result).to.be(1);
|
||||
|
||||
result = parser.parseMilliseconds('1');
|
||||
expect(result).to.be(1);
|
||||
});
|
||||
|
||||
it('should interpret a number with "s" as the unit of measure as seconds', function () {
|
||||
var result = parser.parseMilliseconds('5s');
|
||||
expect(result).to.be(5 * 1000);
|
||||
});
|
||||
|
||||
it('should interpret a number with "second" as the unit of measure as seconds', function () {
|
||||
var result = parser.parseMilliseconds('5second');
|
||||
expect(result).to.be(5 * 1000);
|
||||
});
|
||||
|
||||
it('should interpret a number with "seconds" as the unit of measure as seconds', function () {
|
||||
var result = parser.parseMilliseconds('5seconds');
|
||||
expect(result).to.be(5 * 1000);
|
||||
});
|
||||
|
||||
it('should interpret a number with "m" as the unit of measure as minutes', function () {
|
||||
var result = parser.parseMilliseconds('9m');
|
||||
expect(result).to.be(9 * 1000 * 60);
|
||||
});
|
||||
|
||||
it('should interpret a number with "minute" as the unit of measure as minutes', function () {
|
||||
var result = parser.parseMilliseconds('9minute');
|
||||
expect(result).to.be(9 * 1000 * 60);
|
||||
});
|
||||
|
||||
it('should interpret a number with "minutes" as the unit of measure as minutes', function () {
|
||||
var result = parser.parseMilliseconds('9minutes');
|
||||
expect(result).to.be(9 * 1000 * 60);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('parse function', function () {
|
||||
|
||||
var options;
|
||||
var parser;
|
||||
beforeEach(function () {
|
||||
options = { install: 'dummy/dummy' };
|
||||
});
|
||||
|
||||
it('should require the user to specify either install and remove', function () {
|
||||
options.install = null;
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.throwError(/Please specify either --install or --remove./);
|
||||
});
|
||||
|
||||
it('should not allow the user to specify both install and remove', function () {
|
||||
options.remove = 'package';
|
||||
options.install = 'org/package/version';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.throwError(/Please specify either --install or --remove./);
|
||||
});
|
||||
|
||||
describe('quiet option', function () {
|
||||
|
||||
it('should default to false', function () {
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings.quiet).to.be(false);
|
||||
});
|
||||
|
||||
it('should set settings.quiet property to true', function () {
|
||||
options.parent = { quiet: true };
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings.quiet).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('silent option', function () {
|
||||
|
||||
it('should default to false', function () {
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('silent', false);
|
||||
});
|
||||
|
||||
it('should set settings.silent property to true', function () {
|
||||
options.silent = true;
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('silent', true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('timeout option', function () {
|
||||
|
||||
it('should default to 0 (milliseconds)', function () {
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('timeout', 0);
|
||||
});
|
||||
|
||||
it('should set settings.timeout property to specified value', function () {
|
||||
options.timeout = 1234;
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('timeout', 1234);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('install option', function () {
|
||||
|
||||
it('should set settings.action property to "install"', function () {
|
||||
options.install = 'org/package/version';
|
||||
parser = settingParser(options);
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('action', 'install');
|
||||
});
|
||||
|
||||
it('should allow two parts to the install parameter', function () {
|
||||
options.install = 'kibana/test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.not.throwError();
|
||||
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('organization', 'kibana');
|
||||
expect(settings).to.have.property('package', 'test-plugin');
|
||||
expect(settings).to.have.property('version', undefined);
|
||||
});
|
||||
|
||||
it('should allow three parts to the install parameter', function () {
|
||||
options.install = 'kibana/test-plugin/v1.0.1';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.not.throwError();
|
||||
|
||||
var settings = parser.parse(options);
|
||||
|
||||
expect(settings).to.have.property('organization', 'kibana');
|
||||
expect(settings).to.have.property('package', 'test-plugin');
|
||||
expect(settings).to.have.property('version', 'v1.0.1');
|
||||
});
|
||||
|
||||
it('should not allow one part to the install parameter', function () {
|
||||
options.install = 'test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.throwError(/Invalid install option. Please use the format <org>\/<plugin>\/<version>./);
|
||||
});
|
||||
|
||||
it('should not allow more than three parts to the install parameter', function () {
|
||||
options.install = 'kibana/test-plugin/v1.0.1/dummy';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.throwError(/Invalid install option. Please use the format <org>\/<plugin>\/<version>./);
|
||||
});
|
||||
|
||||
it('should populate the urls collection properly when no version specified', function () {
|
||||
options.install = 'kibana/test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings.urls).to.have.property('length', 2);
|
||||
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 () {
|
||||
options.install = 'kibana/test-plugin/v1.1.1';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings.urls).to.have.property('length', 2);
|
||||
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 () {
|
||||
options.install = 'kibana/test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
var expected = path.resolve(__dirname, '..', '..', '..', '..', '..', 'src', 'server', 'bin', 'plugins', 'test-plugin');
|
||||
|
||||
expect(settings).to.have.property('pluginPath', expected);
|
||||
});
|
||||
|
||||
describe('with url option', function () {
|
||||
|
||||
it('should allow one part to the install parameter', function () {
|
||||
options.install = 'test-plugin';
|
||||
options.url = 'http://www.google.com/plugin.tar.gz';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs().to.not.throwError();
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings).to.have.property('package', 'test-plugin');
|
||||
});
|
||||
|
||||
it('should not allow more than one part to the install parameter', function () {
|
||||
options.url = 'http://www.google.com/plugin.tar.gz';
|
||||
options.install = 'kibana/test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs()
|
||||
.to.throwError(/Invalid install option. When providing a url, please use the format <plugin>./);
|
||||
});
|
||||
|
||||
it('should result in only the specified url in urls collection', function () {
|
||||
var url = 'http://www.google.com/plugin.tar.gz';
|
||||
options.install = 'test-plugin';
|
||||
options.url = url;
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings).to.have.property('urls');
|
||||
expect(settings.urls).to.be.an('array');
|
||||
expect(settings.urls).to.have.property('length', 1);
|
||||
expect(settings.urls).to.contain(url);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('remove option', function () {
|
||||
|
||||
it('should set settings.action property to "remove"', function () {
|
||||
options.install = null;
|
||||
options.remove = 'package';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings).to.have.property('action', 'remove');
|
||||
});
|
||||
|
||||
it('should allow one part to the remove parameter', function () {
|
||||
options.install = null;
|
||||
options.remove = 'test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
|
||||
expect(settings).to.have.property('package', 'test-plugin');
|
||||
});
|
||||
|
||||
it('should not allow more than one part to the install parameter', function () {
|
||||
options.install = null;
|
||||
options.remove = 'kibana/test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
expect(parser.parse).withArgs()
|
||||
.to.throwError(/Invalid remove option. Please use the format <plugin>./);
|
||||
});
|
||||
|
||||
it('should populate the pluginPath', function () {
|
||||
options.install = null;
|
||||
options.remove = 'test-plugin';
|
||||
parser = settingParser(options);
|
||||
|
||||
var settings = parser.parse();
|
||||
var expected = path.resolve(__dirname, '..', '..', '..', '..', '..', 'src', 'server', 'bin', 'plugins', 'test-plugin');
|
||||
|
||||
expect(settings).to.have.property('pluginPath', expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue