[Merge] Merged with latest hapi server

This commit is contained in:
Khalah Jones-Golden 2015-05-05 10:53:34 -04:00
parent 3773894e71
commit 409bc43baf
259 changed files with 13513 additions and 133510 deletions

View file

@ -7,4 +7,7 @@ indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
insert_final_newline = true
[*.md]
insert_final_newline = false

View file

@ -3,6 +3,7 @@
"node": true,
"globals": {
"Promise": true
"Promise": true,
"status": true
}
}

View file

@ -22,6 +22,13 @@ Please make sure you have signed the [Contributor License Agreement](http://www.
```sh
npm install -g grunt-cli bower
```
- Clone the kibana repo and move into it
```sh
git clone https://github.com/elastic/kibana.git kibana
cd kibana
```
- Install node and bower dependencies

View file

@ -34,7 +34,7 @@ module.exports = function (grunt) {
'Gruntfile.js',
'<%= root %>/tasks/**/*.js',
'<%= src %>/kibana/*.js',
'<%= src %>/server/*.js',
'<%= src %>/server/**/*.js',
'<%= src %>/kibana/{components,directives,factories,filters,plugins,registry,services,utils}/**/*.js',
'<%= unitTestDir %>/**/*.js',
'!<%= unitTestDir %>/specs/vislib/fixture/**/*'

View file

@ -50,7 +50,8 @@
"lodash-deep": "spenceralger/lodash-deep#compat",
"marked": "~0.3.2",
"numeral": "~1.5.3",
"angularjs-nvd3-directives": "~0.0.7"
"angularjs-nvd3-directives": "~0.0.7",
"leaflet-draw": "~0.2.4"
},
"devDependencies": {}
}

View file

@ -2,6 +2,7 @@
= Kibana User Guide
:ref: http://www.elastic.co/guide/en/elasticsearch/reference/current
:shield: https://www.elastic.co/guide/en/shield/current
include::introduction.asciidoc[]

View file

@ -24,19 +24,26 @@ If you are using Shield to authenticate Elasticsearch users, you need to provide
the Kibana server with credentials so it can access the `.kibana` index and monitor
the cluster.
To configure credentials for the Kibana server, set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml`:
To configure credentials for the Kibana server:
----
# If your Elasticsearch is protected with basic auth:
kibana_elasticsearch_username: kibana4
kibana_elasticsearch_password: kibana4
----
For information about assigning the Kibana server the necessary permissions in Shield,
see https://www.elastic.co/guide/en/shield/current/_shield_with_kibana_4.html[Shield with Kibana 4]
. Assign the `kibana4_server` role to a user in Shield. For more information, see
{shield}/_shield_with_kibana_4.html[Configuring a Role for the Kibana 4 Server]
in the Shield documentation.
. Set the `kibana_elasticsearch_username` and
`kibana_elasticsearch_password` properties in `kibana.yml` to specify the credentials
of the user you assigned the `kibana4_server`
role:
+
[source,text]
----
kibana_elasticsearch_username: kibana4-user
kibana_elasticsearch_password: kibana4-password
----
Kibana 4 users also need access to the `.kibana` index so they can save and load searches, visualizations, and dashboards.
For more information, see {shield}/_shield_with_kibana_4.html#kibana4-roles[Configuring Roles for Kibana 4 Users] in the Shield documentation.
[float]
[[enabling-ssl]]
=== Enabling SSL
@ -45,6 +52,7 @@ sends to Elasticsearch.
To encrypt communications between the browser and the Kibana server, you configure the `ssl_key_file `and `ssl_cert_file` properties in `kibana.yml`:
[source,text]
----
# SSL for outgoing requests from the Kibana Server (PEM formatted)
ssl_key_file: /path/to/your/server.key
@ -58,12 +66,15 @@ the Kibana server and Elasticsearch are encrypted.
To do this, you specify the HTTPS
protocol when you configure the Elasticsearch URL in `kibana.yml`:
[source,text]
----
elasticsearch: "https://<your_elasticsearch_host>.com:9200"
----
If you are using a self-signed certificate for Elasticsearch, set the `ca` property in
`kibana.yml` to specify the location of the PEM file. Setting the `ca` property lets you leave the `verify_ssl` option enabled.
[source,text]
----
# If you need to provide a CA certificate for your Elasticsarech instance, put
# the path of the pem file here.

View file

@ -12,7 +12,7 @@
],
"private": false,
"version": "4.1.0-snapshot",
"main": "src/server/app.js",
"main": "src/hapi/index.js",
"homepage": "https://www.elastic.co/products/kibana",
"bugs": {
"url": "http://github.com/elastic/kibana/issues"
@ -57,10 +57,12 @@
"hapi": "^8.4.0",
"http-auth": "^2.2.5",
"jade": "~1.8.2",
"joi": "^6.2.0",
"js-yaml": "^3.2.5",
"json-stringify-safe": "^5.0.0",
"less-middleware": "1.0.x",
"lodash": "^2.4.1",
"lodash-deep": "^1.6.0",
"moment": "^2.9.0",
"morgan": "~1.5.1",
"numeral": "^1.5.3",
@ -96,6 +98,7 @@
"http-proxy": "~1.8.1",
"husky": "~0.6.0",
"istanbul": "~0.2.4",
"libesvm": "0.0.12",
"load-grunt-config": "~0.7.0",
"lodash": "~2.4.1",
"marked": "^0.3.2",
@ -103,8 +106,10 @@
"mkdirp": "^0.5.0",
"mocha": "~1.20.1",
"mocha-screencast-reporter": "~0.1.4",
"nock": "^1.6.0",
"opn": "~1.0.0",
"path-browserify": "0.0.0",
"portscanner": "^1.0.0",
"progress": "^1.1.8",
"requirejs": "~2.1.14",
"rjs-build-analysis": "0.0.3",

65
src/hapi/bin/kibana.js Normal file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
var _ = require('lodash');
var kibana = require('../');
var program = require('commander');
var path = require('path');
var writePidFile = require('../lib/write_pid_file');
var loadSettingsFromYAML = require('../lib/load_settings_from_yaml');
var env = (process.env.NODE_ENV) ? process.env.NODE_ENV : 'development';
var packagePath = path.resolve(__dirname, '..', '..', '..', 'package.json');
if (env !== 'development') {
packagePath = path.resolve(__dirname, '..', 'package.json');
}
var package = require(packagePath);
program.description('Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch.');
program.version(package.version);
program.option('-e, --elasticsearch <uri>', 'Elasticsearch instance');
program.option('-c, --config <path>', 'Path to the config file');
program.option('-p, --port <port>', 'The port to bind to', parseInt);
program.option('-q, --quiet', 'Turns off logging');
program.option('-H, --host <host>', 'The host to bind to');
program.option('-l, --log-file <path>', 'The file to log to');
program.option('--plugins <path>', 'Path to scan for plugins');
program.parse(process.argv);
if (program.plugins) {
config['externalPluginsFolder'] = program.plugins;
}
var settings = {};
if (program.elasticsearch) {
settings['elasticsearch.url'] = program.elasticsearch;
}
if (program.port) {
settings['kibana.server.port'] = program.port;
}
if (program.host) {
settings['kibana.server.host'] = program.host;
}
if (program.quiet) {
settings['logging.quiet'] = program.quiet;
}
if (program.logFile) {
settings['logging.file'] = program.logFile;
}
if (program.config) {
// Create the settings with the overrides from the YAML config file.
settings = _.defaults(settings, loadSettingsFromYAML(program.config));
}
// Start the Kibana server with the settings fromt he CLI and YAML file
kibana.start(settings)
.then(writePidFile)
.catch(function (err) {
process.exit(1);
});

View file

@ -1,53 +0,0 @@
var _ = require('lodash');
var fs = require('fs');
var yaml = require('js-yaml');
var path = require('path');
var listPlugins = require('../lib/list_plugins');
var configPath = process.env.CONFIG_PATH || path.join(__dirname, 'kibana.yml');
var kibana = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
var env = process.env.NODE_ENV || 'development';
function checkPath(path) {
try {
fs.statSync(path);
return true;
} catch (err) {
return false;
}
}
// Check if the local public folder is present. This means we are running in
// the NPM module. If it's not there then we are running in the git root.
var public_folder = path.resolve(__dirname, '..', 'public');
if (!checkPath(public_folder)) public_folder = path.resolve(__dirname, '..', '..', 'kibana');
// Check to see if htpasswd file exists in the root directory otherwise set it to false
var htpasswdPath = path.resolve(__dirname, '..', '..', '.htpasswd');
if (!checkPath(htpasswdPath)) htpasswdPath = path.resolve(__dirname, '..', '..', '..', '..', '.htpasswd');
if (!checkPath(htpasswdPath)) htpasswdPath = false;
var packagePath = path.resolve(__dirname, '..', 'package.json');
try {
fs.statSync(packagePath);
} catch (err) {
packagePath = path.resolve(__dirname, '..', '..', '..', 'package.json');
}
var config = module.exports = {
port : kibana.port || 5601,
host : kibana.host || '0.0.0.0',
elasticsearch : kibana.elasticsearch_url || 'http : //localhost : 9200',
root : path.normalize(path.join(__dirname, '..')),
quiet : false,
public_folder : public_folder,
external_plugins_folder : process.env.PLUGINS_FOLDER || null,
bundled_plugins_folder : path.resolve(public_folder, 'plugins'),
kibana : kibana,
package : require(packagePath),
htpasswd : htpasswdPath,
buildNum : '@@buildNum',
maxSockets : kibana.maxSockets || Infinity,
log_file : kibana.log_file || null
};
config.plugins = listPlugins(config);

View file

@ -1,54 +0,0 @@
# Kibana is served by a back end server. This controls which port to use.
port: 5601
# The host to bind the server to.
host: "0.0.0.0"
# The Elasticsearch instance to use for all your queries.
elasticsearch_url: "http://localhost:9200"
# preserve_elasticsearch_host true will send the hostname specified in `elasticsearch`. If you set it to false,
# then the host you use to connect to *this* Kibana instance will be sent.
elasticsearch_preserve_host: true
# Kibana uses an index in Elasticsearch to store saved searches, visualizations
# and dashboards. It will create a new index if it doesn't already exist.
kibana_index: ".kibana"
# If your Elasticsearch is protected with basic auth, this is the user credentials
# used by the Kibana server to perform maintence on the kibana_index at statup. Your Kibana
# users will still need to authenticate with Elasticsearch (which is proxied thorugh
# the Kibana server)
# kibana_elasticsearch_username: user
# kibana_elasticsearch_password: pass
# The default application to load.
default_app_id: "discover"
# Time in milliseconds to wait for responses from the back end or elasticsearch.
# This must be > 0
request_timeout: 300000
# Time in milliseconds for Elasticsearch to wait for responses from shards.
# Set to 0 to disable.
shard_timeout: 0
# Set to false to have a complete disregard for the validity of the SSL
# certificate.
verify_ssl: true
# If you need to provide a CA certificate for your Elasticsarech instance, put
# the path of the pem file here.
# ca: /path/to/your/CA.pem
# SSL for outgoing requests from the Kibana Server (PEM formatted)
# ssl_key_file: /path/to/your/server.key
# ssl_cert_file: /path/to/your/server.crt
# Set the path to where you would like the process id file to be created.
# pid_file: /var/run/kibana.pid
# If you would like to send the log output to a file you can set the path below.
# This will also turn off the STDOUT log output.
# log_file: ./kibana.log

View file

@ -1,5 +1,5 @@
module.exports.extendHapi = require('./lib/extend_hapi');
module.exports.Plugin = require('./lib/plugin');
module.exports.Plugin = require('./lib/plugins/plugin');
module.exports.start = require('./lib/start');
if (require.main === module) {

View file

@ -1,3 +0,0 @@
module.exports = function () {
return require('../config');
};

View file

@ -0,0 +1,10 @@
var fs = require('fs');
module.exports = function checkPath(path) {
try {
fs.statSync(path);
return true;
} catch (err) {
return false;
}
};

View file

@ -0,0 +1,81 @@
var Promise = require('bluebird');
var Joi = require('joi');
var _ = require('lodash');
var override = require('./override');
_.mixin(require('lodash-deep'));
function Config(schema, config) {
config = config || {};
this.schema = schema || Joi.object({}).default();
this.reset(config);
}
Config.prototype.extendSchema = function (key, schema) {
var additionalSchema = {};
if (!this.has(key)) {
additionalSchema[key] = schema;
this.schema = this.schema.keys(additionalSchema);
this.reset(this.config);
}
};
Config.prototype.reset = function (obj) {
var results = Joi.validate(obj, this.schema);
if (results.error) {
throw results.error;
}
this.config = results.value;
};
Config.prototype.set = function (key, value) {
var config = _.cloneDeep(this.config);
if (_.isPlainObject(key)) {
config = override(config, key);
} else {
_.deepSet(config, key, value);
}
var results = Joi.validate(config, this.schema);
if (results.error) {
throw results.error;
}
this.config = results.value;
};
Config.prototype.get = function (key) {
if (!key) {
return _.cloneDeep(this.config);
}
var value = _.deepGet(this.config, key);
if (value === undefined) {
if (!this.has(key)) {
throw new Error('Unknown config key: ' + key);
}
}
return _.cloneDeep(value);
};
Config.prototype.has = function (key) {
function has(key, schema, path) {
path = path || [];
// Catch the partial paths
if (path.join('.') === key) return true;
// Only go deep on inner objects with children
if (schema._inner.children.length) {
for (var i = 0; i < schema._inner.children.length; i++) {
var child = schema._inner.children[i];
// If the child is an object recurse through it's children and return
// true if there's a match
if (child.schema._type === 'object') {
if (has(key, child.schema, path.concat([child.key]))) return true;
// if the child matches, return true
} else if (path.concat([child.key]).join('.') === key) {
return true;
}
}
}
}
return !!has(key, this.schema);
};
module.exports = Config;

View file

@ -0,0 +1,19 @@
var _ = require('lodash');
module.exports = function (dot, flatObject) {
var fullObject = {};
_.each(flatObject, function (value, key) {
var keys = key.split(dot);
(function walk(memo, keys, value) {
var _key = keys.shift();
if (keys.length === 0) {
memo[_key] = value;
} else {
if (!memo[_key]) memo[_key] = {};
walk(memo[_key], keys, value);
}
})(fullObject, keys, value);
});
return fullObject;
};

View file

@ -0,0 +1,18 @@
var _ = require('lodash');
module.exports = function (dot, nestedObj, flattenArrays) {
var key; // original key
var stack = []; // track key stack
var flatObj = {};
(function flattenObj(obj) {
_.keys(obj).forEach(function (key) {
stack.push(key);
if (!flattenArrays && _.isArray(obj[key])) flatObj[stack.join(dot)] = obj[key];
else if (_.isObject(obj[key])) flattenObj(obj[key]);
else flatObj[stack.join(dot)] = obj[key];
stack.pop();
});
}(nestedObj));
return flatObj;
};

View file

@ -0,0 +1,6 @@
var Config = require('./config');
var schema = require('./schema');
var config = new Config(schema);
module.exports = function () {
return config;
};

View file

@ -0,0 +1,11 @@
var _ = require('lodash');
var flattenWith = require('./flatten_with');
var explodeBy = require('./explode_by');
module.exports = function (target, source) {
var _target = flattenWith('.', target);
var _source = flattenWith('.', source);
return explodeBy('.', _.defaults(_source, _target));
};

View file

@ -0,0 +1,71 @@
var Joi = require('joi');
var fs = require('fs');
var path = require('path');
var checkPath = require('./check_path');
var packagePath = path.resolve(__dirname, '..', '..', 'package.json');
// Check if the local public folder is present. This means we are running in
// the NPM module. If it's not there then we are running in the git root.
var publicFolder = path.resolve(__dirname, '..', '..', 'public');
if (!checkPath(publicFolder)) publicFolder = path.resolve(__dirname, '..', '..', '..', 'kibana');
try {
fs.statSync(packagePath);
} catch (err) {
packagePath = path.resolve(__dirname, '..', '..', '..', '..', 'package.json');
}
var bundledPluginsFolder = path.resolve(publicFolder, 'plugins');
module.exports = Joi.object({
kibana: Joi.object({
server: Joi.object({
host: Joi.string().hostname().default('0.0.0.0'),
port: Joi.number().default(5601),
maxSockets: Joi.any().default(Infinity),
pidFile: Joi.string(),
root: Joi.string().default(path.normalize(path.join(__dirname, '..'))),
ssl: Joi.object({
cert: Joi.string(),
key: Joi.string()
}).default()
}).default(),
index: Joi.string().default('.kibana'),
publicFolder: Joi.string().default(publicFolder),
externalPluginsFolder: Joi.alternatives().try(Joi.array().items(Joi.string()), Joi.string()),
bundledPluginsFolder: Joi.string().default(bundledPluginsFolder),
defaultAppId: Joi.string().default('discover'),
package: Joi.any().default(require(packagePath)),
buildNum: Joi.string().default('@@buildNum'),
bundledPluginIds: Joi.array().items(Joi.string())
}).default(),
elasticsearch: Joi.object({
url: Joi.string().uri({ scheme: ['http', 'https'] }).default('http://localhost:9200'),
preserveHost: Joi.boolean().default(true),
username: Joi.string(),
password: Joi.string(),
shardTimeout: Joi.number().default(0),
requestTimeout: Joi.number().default(30000),
pingTimeout: Joi.number().default(30000),
startupTimeout: Joi.number().default(5000),
ssl: Joi.object({
verify: Joi.boolean().default(true),
ca: Joi.string(),
cert: Joi.string(),
key: Joi.string()
}).default(),
}).default(),
logging: Joi.object({
quiet: Joi.boolean().default(false),
file: Joi.string(),
console: Joi.object({
ops: Joi.any(),
log: Joi.any().default('*'),
response: Joi.any().default('*'),
error: Joi.any().default('*'),
json: Joi.boolean().default(false),
}).default()
}).default(),
}).default();

View file

@ -1,4 +1,4 @@
module.exports = function (server) {
server.decorate('server', 'config', require('./config'));
server.decorate('server', 'loadKibanaPlugins', require('./load_kibana_plugins'));
server.decorate('server', 'loadKibanaPlugins', require('./plugins/load_kibana_plugins'));
};

View file

@ -1,19 +0,0 @@
var _ = require('lodash');
var glob = require('glob');
var path = require('path');
var plugins = function (dir) {
if (!dir) return [];
var files = glob.sync(path.join(dir, '*', 'index.js')) || [];
return files.map(function (file) {
return file.replace(dir, 'plugins').replace(/\.js$/, '');
});
};
module.exports = function (config) {
var bundled_plugin_ids = config.kibana.bundled_plugin_ids || [];
var bundled_plugins = plugins(config.bundled_plugins_folder);
var external_plugins = plugins(config.external_plugins_folder);
return bundled_plugin_ids.concat(bundled_plugins, external_plugins);
};

View file

@ -1,11 +0,0 @@
var _ = require('lodash');
var Promise = require('bluebird');
var registerPlugins = require('./register_plugins');
var requirePlugins = require('./require_plugins');
var setupLogging = require('./setup_logging');
module.exports = function (externalPlugins) {
var plugins = requirePlugins().concat(externalPlugins);
return setupLogging(this).then(function (server) {
registerPlugins(server, plugins);
});
};

View file

@ -0,0 +1,28 @@
var fs = require('fs');
var yaml = require('js-yaml');
module.exports = function (path) {
var config = yaml.safeLoad(fs.readFileSync(path, 'utf8'));
var settings = {};
if (config.port) settings['kibana.server.port'] = config.port;
if (config.host) settings['kibana.server.host'] = config.host;
if (config.elasticsearch_url) settings['elasticsearch.url'] = config.elasticsearch_url;
if (config.elasticsearch_preserve_host) settings['elasticsearch.preserveHost'] = config.elasticsearch_preserve_host;
if (config.config_index) settings['config.index'] = config.config_index;
if (config.config_elasticsearch_username) settings['elasticsearch.username'] = config.config_elasticsearch_username;
if (config.config_elasticsearch_password) settings['elasticsearch.password'] = config.config_elasticsearch_password;
if (config.config_elasticsearch_client_crt) settings['elasticsearch.ssl.cert'] = config.config_elasticsearch_client_crt;
if (config.config_elasticsearch_client_key) settings['elasticsearch.ssl.key'] = config.config_elasticsearch_client_key;
if (config.ca) settings['elasticsearch.ssl.ca'] = config.ca;
if (config.verify_ssl) settings['elasticsearch.ssl.verify'] = config.verify_ssl;
if (config.default_app_id) settings['kibana.defaultAppId'] = config.default_app_id;
if (config.ping_timeout) settings['elastcsearch.pingTimeout'] = config.ping_timeout;
if (config.request_timeout) settings['elastcsearch.requestTimeout'] = config.request_timeout;
if (config.shard_timeout) settings['elastcsearch.shardTimeout'] = config.shard_timeout;
if (config.startup_timeout) settings['elastcsearch.startupTimeout'] = config.startup_timeout;
if (config.ssl_cert_file) settings['kibana.server.ssl.cert'] = config.ssl_cert_file;
if (config.ssl_key_file) settings['kibana.server.ssl.key'] = config.ssl_key_file;
if (config.pid_file) settings['config.server.pidFile'] = config.pid_file;
if (config.log_file) settings['logging.file'] = config.log_file;
return settings;
};

View file

@ -0,0 +1,35 @@
var Promise = require('bluebird');
var good = require('good');
var path = require('path');
var join = path.join;
var Console = require('./good_reporters/console');
module.exports = function (server) {
return new Promise(function (resolve, reject) {
var reporters = [];
var config = server.config();
// If we are not quite then add the console logger
var filters = {};
if (!config.get('logging.quiet')) {
if (config.get('logging.console.ops') != null) filters.ops = config.get('logging.console.ops');
if (config.get('logging.console.log') != null) filters.log = config.get('logging.console.log');
if (config.get('logging.console.response') != null) filters.response = config.get('logging.console.response');
if (config.get('logging.console.error') != null) filters.error = config.get('logging.console.error');
}
reporters.push({ reporter: Console, args: [filters, { json: config.get('logging.console.json') } ] });
server.register({
register: good,
options: {
opsInterval: 5000,
logRequestHeaders: true,
logResponsePayload: true,
reporters: reporters
}
}, function (err) {
if (err) return reject(err);
resolve(server);
});
});
};

View file

@ -1,17 +0,0 @@
var _ = require('lodash');
var Promise = require('bluebird');
var getStatus = require('./get_status');
var setStatus = require('./set_status');
var util = require('util');
function Plugin(options) {
options = _.defaults(options, {
require: [],
init: function (server, options) {
return Promise.reject(new Error('You must override the init function for plugins'));
}
});
_.assign(this, options);
}
module.exports = Plugin;

View file

@ -0,0 +1,15 @@
var Promise = require('bluebird');
module.exports = function (plugin) {
if (plugin.publicPath) {
plugin.server.route({
method: 'GET',
path: '/' + plugin.name + '/{paths*}',
handler: {
directory: {
path: plugin.publicPath
}
}
});
}
return Promise.resolve(plugin);
};

View file

@ -0,0 +1,24 @@
var _ = require('lodash');
var glob = require('glob');
var path = require('path');
var plugins = function (dir) {
if (!dir) return [];
var files = glob.sync(path.join(dir, '*', 'index.js')) || [];
return files.map(function (file) {
return file.replace(dir, 'plugins').replace(/\.js$/, '');
});
};
var cache;
module.exports = function (config) {
if (!cache) {
var bundled_plugin_ids = config.get('kibana.bundledPluginIds') || [];
var bundled_plugins = plugins(config.get('kibana.bundledPluginsFolder'));
var external_plugins = plugins(config.get('kibana.externalPluginsFolder'));
cache = bundled_plugin_ids.concat(bundled_plugins, external_plugins);
}
return cache;
};

View file

@ -0,0 +1,21 @@
var Promise = require('bluebird');
var registerPlugins = require('./register_plugins');
var requirePlugins = require('./require_plugins');
var logging = require('../logging/');
var registerPluginConfigs = require('./register_plugin_configs');
module.exports = function (externalPlugins) {
// require all the internal plugins then concat witht the external
// plugins passed in from the start method.
var plugins = requirePlugins().concat(externalPlugins);
// setup logging then register the plugins
return logging(this)
// Setup the config schema for the plugins
.then(function (server) {
return registerPluginConfigs(server, plugins);
})
// Register the plugins
.then(function (server) {
return registerPlugins(server, plugins);
});
};

View file

@ -0,0 +1,18 @@
var _ = require('lodash');
var Promise = require('bluebird');
function Plugin(options) {
this.server = null;
this.status = null;
this.publicPath = null;
this.require = [];
this.init = function (server, options) {
return Promise.reject(new Error('You must override the init function for plugins'));
};
this.config = function (Joi) {
return Joi.object({}).default();
};
_.assign(this, options);
}
module.exports = Plugin;

View file

@ -0,0 +1,22 @@
var Promise = require('bluebird');
var Joi = require('joi');
/**
* Execute the #config() call on each of the plugins and attach their schema's
* to the main config object under their namespace.
* @param {object} server Kibana server
* @param {array} plugins Plugins for Kibana
* @returns {Promise}
*/
module.exports = function (server, plugins) {
var config = server.config();
return Promise.each(plugins, function (plugin) {
return Promise.resolve(plugin.config(Joi)).then(function (schema) {
var pluginSchema = {};
if (schema) {
config.extendSchema(plugin.name, schema);
}
});
}).then(function () {
return server;
});
};

View file

@ -1,11 +1,17 @@
var _ = require('lodash');
var Promise = require('bluebird');
var checkDependencies = require('./check_dependencies');
var systemStatus = require('./system_status');
var status = require('../status');
var addStaticsForPublic = require('./add_statics_for_public');
function checkForCircularDependency(tasks) {
/**
* Check to see if there are any circular dependencies for the task tree
* @param {array} plugins an array of plugins
* @returns {type} description
*/
function checkForCircularDependency(plugins) {
var deps = {};
tasks.forEach(function (task) {
plugins.forEach(function (task) {
deps[task.name] = [];
if (task.require) deps[task.name] = task.require;
});
@ -23,6 +29,11 @@ module.exports = function (server, plugins) {
var finished = false;
var todo = plugins.concat();
/**
* Checks to see if all the tasks are completed for an array of dependencies
* @param {array} tasks An array of plugin names
* @returns {boolean} if all the tasks are done this it will return true
*/
function allDone(tasks) {
var done = _.keys(results);
return tasks.every(function (dep) {
@ -30,21 +41,29 @@ module.exports = function (server, plugins) {
});
}
/**
* Register a plugin with the Kibana server
*
* This includes setting up the status object and setting the reference to
* the plugin's server
*
* @param {object} plugin The plugin to register
* @returns {Promise}
*/
function registerPlugin(plugin) {
var config = server.config();
return new Promise(function (resolve, reject) {
var register = function (server, options, next) {
plugin.server = server;
systemStatus.createStatus(plugin);
plugin.status.yellow('Initializing');
status.createStatus(plugin);
Promise.try(plugin.init, [server, options], plugin).nodeify(next);
};
register.attributes = { name: plugin.name };
var options = config[plugin.name] || {};
var options = config.get(plugin.name) || {};
server.register({ register: register, options: options }, function (err) {
if (err) return reject(err);
resolve();
plugin.status.green('Ready');
resolve(plugin);
});
});
}
@ -64,6 +83,7 @@ module.exports = function (server, plugins) {
if (!plugin.require || (plugin.require && allDone(plugin.require))) {
running[plugin.name] = true;
registerPlugin(plugin)
.then(addStaticsForPublic)
.then(function () {
results[plugin.name] = true;
runPending();

View file

@ -2,9 +2,10 @@ var path = require('path');
var join = path.join;
var glob = require('glob');
var Promise = require('bluebird');
var checkPath = require('../config/check_path');
module.exports = function (globPath) {
globPath = globPath || join( __dirname, '..', 'plugins', '*', 'index.js');
globPath = globPath || join( __dirname, '..', '..', 'plugins', '*', 'index.js');
return glob.sync(globPath).map(function (file) {
var module = require(file);
var regex = new RegExp('([^' + path.sep + ']+)' + path.sep + 'index.js');
@ -12,6 +13,12 @@ module.exports = function (globPath) {
if (!module.name && matches) {
module.name = matches[1];
}
// has a public folder?
var publicPath = join(path.dirname(file), 'public');
if (checkPath(publicPath)) {
module.publicPath = publicPath;
}
return module;
});
};

View file

@ -1,29 +0,0 @@
var Promise = require('bluebird');
var good = require('good');
var path = require('path');
var join = path.join;
var Console = require('./good_reporters/console');
var reporters = [
{
reporter: Console,
args: [{ ops: '*', log: '*', response: '*', error: '*' }, { json: false }]
}
];
module.exports = function (server) {
return new Promise(function (resolve, reject) {
server.register({
register: good,
options: {
opsInterval: 5000,
logRequestHeaders: true,
logResponsePayload: true,
reporters: reporters
}
}, function (err) {
if (err) return reject(err);
resolve(server);
});
});
};

View file

@ -1,10 +1,13 @@
var _ = require('lodash');
var Promise = require('bluebird');
var Hapi = require('hapi');
var requirePlugins = require('./require_plugins');
var validatePlugin = require('./validate_plugin');
var requirePlugins = require('./plugins/require_plugins');
var validatePlugin = require('./plugins/validate_plugin');
var extendHapi = require('./extend_hapi');
var join = require('path').join;
module.exports = function (plugins) {
module.exports = function (settings, plugins) {
// Plugin authors can use this to add plugins durring development
plugins = plugins || [];
@ -18,13 +21,24 @@ module.exports = function (plugins) {
// Extend Hapi with Kibana
extendHapi(server);
var config = server.config();
if (settings) config.set(settings);
// Create a new connection
server.connection({ host: server.config().host, port: server.config().port });
server.connection({
host: config.get('kibana.server.host'),
port: config.get('kibana.server.port')
});
// Load external plugins
var externalPlugins = [];
if (server.config().external_plugins_folder) {
externalPlugins = requirePlugins(server.config().external_plugins_folder);
var externalPluginsFolder = config.get('kibana.externalPluginsFolder');
if (externalPluginsFolder) {
externalPlugins = _([externalPluginsFolder])
.flatten()
.map(requirePlugins)
.flatten()
.value();
}
// Load the plugins
@ -41,6 +55,7 @@ module.exports = function (plugins) {
})
.catch(function (err) {
server.log('fatal', err);
console.log(err.stack);
return Promise.reject(err);
});
};

View file

@ -10,6 +10,7 @@ SystemStatus.prototype.createStatus = function (plugin) {
plugin.server.expose('status', plugin.status);
plugin.status.on('change', logStatusChange(plugin));
this.data[plugin.name] = plugin.status;
plugin.status.yellow('Initializing');
};
SystemStatus.prototype.toJSON = function () {

View file

@ -19,7 +19,7 @@ function createStatusFn(color) {
this.state = color;
this.message = message;
if (previous.state === this.state && previous.message === this.message) return;
this.emit(color, message);
this.emit(color, message, previous);
this.emit('change', this.toJSON(), previous);
};
}

View file

@ -0,0 +1,16 @@
var fs = require('fs');
var Promise = require('bluebird');
module.exports = function (server) {
return new Promise(function (resolve, reject) {
var config = server.config();
var pidFile = config.get('kibana.server.pidFile');
if (!pidFile) return resolve(server);
fs.writeFile(pidFile, process.pid, function (err) {
if (err) {
server.log('error', { err: err });
return reject(err);
}
resolve(server);
});
});
};

View file

@ -1,6 +1,7 @@
var _ = require('lodash');
var Promise = require('bluebird');
var kibana = require('../../');
var listPlugins = require('../../lib/plugins/list_plugins');
module.exports = new kibana.Plugin({
init: function (server, options) {
@ -10,14 +11,12 @@ module.exports = new kibana.Plugin({
path: '/config',
handler: function (request, reply) {
var config = server.config();
var keys = [
'kibana_index',
'default_app_id',
'shard_timeout'
];
var data = _.pick(config.kibana, keys);
data.plugins = config.plugins;
reply(data);
reply({
kibana_index: config.get('kibana.index'),
default_app_id: config.get('kibana.defaultAppId'),
shard_timeout: config.get('elasticsearch.shardTimeout'),
plugins: listPlugins(config)
});
}
});

View file

@ -1,10 +1,12 @@
var url = require('url');
var http = require('http');
var fs = require('fs');
var resolve = require('url').resolve;
var querystring = require('querystring');
var kibana = require('../../');
var healthCheck = require('./lib/health_check');
var exposeClient = require('./lib/expose_client');
var createProxy = require('./lib/create_proxy');
module.exports = new kibana.Plugin({
@ -17,44 +19,28 @@ module.exports = new kibana.Plugin({
// Set up the health check service
healthCheck(this, server);
var target = url.parse(config.elasticsearch);
createProxy(server, 'GET', '/elasticsearch/{paths*}');
createProxy(server, 'POST', '/elasticsearch/_mget');
createProxy(server, 'POST', '/elasticsearch/_msearch');
var agentOptions = {
rejectUnauthorized: config.kibana.verify_ssl
};
var customCA;
if (/^https/.test(target.protocol) && config.kibana.ca) {
customCA = fs.readFileSync(config.kibana.ca, 'utf8');
agentOptions.ca = [customCA];
}
// Add client certificate and key if required by elasticsearch
if (/^https/.test(target.protocol) &&
config.kibana.kibana_elasticsearch_client_crt &&
config.kibana.kibana_elasticsearch_client_key) {
agentOptions.crt = fs.readFileSync(config.kibana.kibana_elasticsearch_client_crt, 'utf8');
agentOptions.key = fs.readFileSync(config.kibana.kibana_elasticsearch_client_key, 'utf8');
}
server.route({
method: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
path: '/elasticsearch/{path*}',
handler: {
proxy: {
mapUri: function (request, callback) {
var url = config.elasticsearch;
if (!/\/$/.test(url)) url += '/';
if (request.params.path) url += request.params.path;
var query = querystring.stringify(request.query);
if (query) url += '?' + query;
callback(null, url);
},
passThrough: true,
agent: new http.Agent(agentOptions)
}
function noBulkCheck(request, reply) {
if (/\/_bulk/.test(request.path)) {
return reply({
error: 'You can not send _bulk requests to this interface.'
}).code(400).takeover();
}
});
return reply.continue();
}
createProxy(
server,
['PUT', 'POST', 'DELETE'],
'/elasticsearch/' + config.get('kibana.index') + '/{paths*}',
{
prefix: '/' + config.get('kibana.index'),
config: { pre: [ noBulkCheck ] }
}
);
}
});

View file

@ -0,0 +1,30 @@
var url = require('url');
var fs = require('fs');
var http = require('http');
var agentOptions;
module.exports = function (server) {
var config = server.config();
var target = url.parse(config.get('elasticsearch.url'));
if (!agentOptions) {
agentOptions = {
rejectUnauthorized: config.get('elasticsearch.ssl.verify')
};
var customCA;
if (/^https/.test(target.protocol) && config.get('elasticsearch.ssl.ca')) {
customCA = fs.readFileSync(config.get('elasticsearch.ssl.ca'), 'utf8');
agentOptions.ca = [customCA];
}
// Add client certificate and key if required by elasticsearch
if (/^https/.test(target.protocol) &&
config.get('elasticsearch.ssl.cert') &&
config.get('elasticsearch.ssl.key')) {
agentOptions.crt = fs.readFileSync(config.get('elasticsearch.ssl.cert'), 'utf8');
agentOptions.key = fs.readFileSync(config.get('elasticsearch.ssl.key'), 'utf8');
}
}
return new http.Agent(agentOptions);
};

View file

@ -0,0 +1,19 @@
var createAgent = require('./create_agent');
var mapUri = require('./map_uri');
module.exports = function createProxy(server, method, route, opts) {
opts = opts || {};
var options = {
method: method,
path: route,
handler: {
proxy: {
mapUri: mapUri(server, opts.prefix),
passThrough: true,
agent: createAgent(server)
}
}
};
if (opts && opts.config) options.config = opts.config;
server.route(options);
};

View file

@ -6,13 +6,13 @@ var url = require('url');
module.exports = function (server) {
var config = server.config();
var uri = url.parse(config.elasticsearch);
var username = config.kibana.kibana_elasticsearch_username;
var password = config.kibana.kibana_elasticsearch_password;
var verify_ssl = config.kibana.verify_ssl;
var client_crt = config.kibana.kibana_elasticsearch_client_crt;
var client_key = config.kibana.kibana_elasticsearch_client_key;
var ca = config.kibana.ca;
var uri = url.parse(config.get('elasticsearch.url'));
var username = config.get('elasticsearch.username');
var password = config.get('elasticsearch.password');
var verify_ssl = config.get('elasticsearch.ssl.verify');
var client_crt = config.get('elasticsearch.ssl.cert');
var client_key = config.get('elasticsearch.ssl.key');
var ca = config.get('elasticsearch.ssl.ca');
if (username && password) {
uri.auth = util.format('%s:%s', username, password);

View file

@ -15,7 +15,7 @@ module.exports = function (plugin, server) {
return client.ping({ requestTimeout: 1500 }).catch(function (err) {
if (!(err instanceof NoConnections)) throw err;
plugin.status.red(format('Unable to connect to Elasticsearch at %s. Retrying in 2.5 seconds.', config.elasticsearch));
plugin.status.red(format('Unable to connect to Elasticsearch at %s. Retrying in 2.5 seconds.', config.get('elasticsearch.url')));
return Promise.delay(2500).then(waitForPong);
});
@ -24,7 +24,7 @@ module.exports = function (plugin, server) {
function waitForShards() {
return client.cluster.health({
timeout: '5s', // tells es to not sit around and wait forever
index: config.kibana.kibana_index
index: config.get('kibana.index')
})
.then(function (resp) {
// if "timed_out" === true then elasticsearch could not

View file

@ -0,0 +1,20 @@
var querystring = require('querystring');
var resolve = require('url').resolve;
module.exports = function mapUri(server, prefix) {
var config = server.config();
return function (request, done) {
var paths = request.params.paths;
if (!paths) {
paths = request.path.replace('/elasticsearch', '');
}
if (prefix) {
paths = prefix + '/' + paths;
}
var url = config.get('elasticsearch.url');
if (!/\/$/.test(url)) url += '/';
if (paths) url = resolve(url, paths);
var query = querystring.stringify(request.query);
if (query) url += '?' + query;
done(null, url);
};
};

View file

@ -0,0 +1,112 @@
var _ = require('lodash');
var parse = require('url').parse;
validate.Fail = function (index) {
this.message = 'Kibana only support modifying the "' + index +
'" index. Requests that might modify other indicies are not sent to elasticsearch.';
};
validate.BadIndex = function (index) {
validate.Fail.call(this, index);
this.message = 'Bad index "' + index + '" in request. ' + this.message;
};
function validate(server, req) {
var config = server.config();
var method = req.method.toUpperCase();
if (method === 'GET' || method === 'HEAD') return true;
var segments = _.compact(parse(req.path).pathname.split('/'));
var maybeIndex = _.first(segments);
var maybeMethod = _.last(segments);
var add = (method === 'POST' || method === 'PUT');
var rem = (method === 'DELETE');
// everything below this point assumes a destructive request of some sort
if (!add && !rem) throw new validate.Fail(config.get('kibana.index'));
var bodyStr = String(req.payload);
var jsonBody = bodyStr && parseJson(bodyStr);
var bulkBody = bodyStr && parseBulk(bodyStr);
// methods that accept standard json bodies
var maybeMGet = ('_mget' === maybeMethod && add && jsonBody);
var maybeSearch = ('_search' === maybeMethod && add);
var maybeValidate = ('_validate' === maybeMethod && add);
// methods that accept bulk bodies
var maybeBulk = ('_bulk' === maybeMethod && add && bulkBody);
var maybeMsearch = ('_msearch' === maybeMethod && add && bulkBody);
// indication that this request is against kibana
var maybeKibanaIndex = (maybeIndex === config.get('kibana.index'));
if (!maybeBulk) validateNonBulkDestructive();
else validateBulkBody(bulkBody);
return true;
function parseJson(str) {
try {
return JSON.parse(str);
} catch (e) {
return;
}
}
function parseBulk(str) {
var parts = str.split(/\r?\n/);
var finalLine = parts.pop();
var evenJsons = (parts.length % 2 === 0);
if (finalLine !== '' || !evenJsons) return;
var body = new Array(parts.length);
for (var i = 0; i < parts.length; i++) {
var part = parseJson(parts[i]);
if (!part) throw new validate.Fail(config.get('kibana.index'));
body[i] = part;
}
return body;
}
function stringifyBulk(body) {
return body.map(JSON.stringify).join('\n') + '\n';
}
function validateNonBulkDestructive() {
// allow any destructive request against the kibana index
if (maybeKibanaIndex) return;
// allow json bodies sent to _mget _search and _validate
if (maybeMGet || maybeSearch || maybeValidate) return;
// allow bulk bodies sent to _msearch
if (maybeMsearch) return;
throw new validate.Fail(config.get('kibana.index'));
}
function validateBulkBody(body) {
while (body.length) {
var header = body.shift();
var req = body.shift();
var op = _.keys(header).join('');
var meta = header[op];
if (!meta) throw new validate.Fail(config.get('kibana.index'));
var index = meta._index || maybeIndex;
if (index !== config.get('kibana.index')) {
throw new validate.BadIndex(index);
}
}
}
}
module.exports = validate;

View file

@ -8,7 +8,7 @@ module.exports = new kibana.Plugin({
path: '/{param*}',
handler: {
directory: {
path: config.public_folder
path: config.get('kibana.publicFolder')
}
}
});

View file

@ -1,18 +1,7 @@
var join = require('path').join;
var kibana = require('../../');
var systemStatus = require('../../lib/system_status');
function Series(size) {
this.size = size;
this.data = [];
}
Series.prototype.push = function (value) {
this.data.unshift([Date.now(), value]);
if (this.data.length > this.size) this.data.pop();
};
Series.prototype.toJSON = function () {
return this.data;
};
var status = require('../../lib/status');
var Series = require('./lib/series');
module.exports = new kibana.Plugin({
@ -33,7 +22,7 @@ module.exports = new kibana.Plugin({
};
server.plugins.good.monitor.on('ops', function (event) {
var port = String(config.port);
var port = String(config.get('kibana.server.port'));
fiveMinuteData.rss.push(event.psmem.rss);
fiveMinuteData.heapTotal.push(event.psmem.heapTotal);
fiveMinuteData.heapUsed.push(event.psmem.heapUsed);
@ -56,23 +45,13 @@ module.exports = new kibana.Plugin({
}
});
server.route({
method: 'GET',
path: '/status/{param*}',
handler: {
directory: {
path: join(__dirname, 'public')
}
}
});
server.route({
method: 'GET',
path: '/status/health',
handler: function (request, reply) {
return reply({
metrics: fiveMinuteData,
status: systemStatus
status: status
});
}
});

View file

@ -0,0 +1,15 @@
function Series(size) {
this.size = size;
this.data = [];
}
Series.prototype.push = function (value) {
this.data.unshift([Date.now(), value]);
if (this.data.length > this.size) this.data.pop();
};
Series.prototype.toJSON = function () {
return this.data;
};
module.exports = Series;

View file

@ -1,5 +1,6 @@
define(function (require) {
var decodeGeoHash = require('utils/decode_geo_hash');
var _ = require('lodash');
function readRows(table, agg, index, chart) {
var geoJson = chart.geoJson;
@ -8,6 +9,7 @@ define(function (require) {
props.length = table.rows.length;
props.min = null;
props.max = null;
props.agg = agg;
table.rows.forEach(function (row) {
var geohash = row[index.geo].value;

View file

@ -9,7 +9,7 @@
</span>
<div class="hintbox" ng-show="showAnalyzedFieldWarning">
<p>
<strong>Careful!</strong> The field selected contains analyzed strings. Values such as <i>foo-bar</i> will be broken into <i>foo</i> and <i>bar</i>. See <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-core-types.html" target="_blank">Mapping Core Types</a> for more information on setting this field as <i>not_analyzed</i>
<strong>Careful!</strong> The field selected contains analyzed strings. Analyzed strings are highly unique and can use a lot of memory to visualize. Values such as <i>foo-bar</i> will be broken into <i>foo</i> and <i>bar</i>. See <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-core-types.html" target="_blank">Mapping Core Types</a> for more information on setting this field as <i>not_analyzed</i>
</p>
<p ng-show="indexedFields.byName[agg.params.field.name + '.raw'].analyzed == false">
@ -22,9 +22,17 @@
name="field"
required
ng-model="agg.params.field"
ng-if="indexedFields.length"
auto-select-if-only-one="indexedFields"
ng-options="field as field.displayName group by field.type for field in indexedFields"
ng-change="aggParam.onChange(agg)">
</select>
<div class="hintbox" ng-if="!indexedFields.length">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>No Compatible Fields:</strong> The "{{ vis.indexPattern.id }}" index pattern does not any of the following field types: {{ agg.type.params.byName.field.filterFieldTypes | commaList:false }}
</p>
</div>
</div>

View file

@ -17,6 +17,15 @@
</div>
</div>
</div>
<input ng-model="agg.params.filters.length" name="filterLength" required min="1" type="number" class="ng-hide">
<div class="hintbox" ng-show="aggForm.filterLength.$invalid">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You must specify at least one filter
</p>
</div>
<div
click-focus="'filter'+(agg.params.filters.length-1)"
ng-click="agg.params.filters.push({input:{}})"

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Values</label>
<kbn-number-list
ng-model="agg.params.values"
unit-name="value"
range="[-Infinity,Infinity]"
>
</kbn-number-list>
</div>

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Percents</label>
<kbn-number-list
ng-model="agg.params.percents"
unit-name="percent"
range="[0,100]"
>
</kbn-number-list>
</div>

View file

@ -1,31 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.percents.controller">
<label>Percentiles</label>
<div
ng-repeat="value in agg.params.percents track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.percents[$index]"
values-list="agg.params.percents"
values-list-min="0"
values-list-max="100"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<p ng-show="aggForm.validLength.$invalid" class="text-danger text-center">
You mush specify at least one percentile
</p>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add Percent
</button>
</div>

View file

@ -3,7 +3,11 @@
<label>JSON Input</label>
<i class="fa fa-info-circle"></i>
</span>
<div class="hintbox" ng-show="showJsonHint">Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example <i>shard_size</i> on a <i>terms</i> aggregation</div>
<div class="hintbox" ng-show="showJsonHint">
<p>
Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example <i>shard_size</i> on a <i>terms</i> aggregation
</p>
</div>
<p>
<textarea
type="text"
@ -12,4 +16,4 @@
validate-json
></textarea>
</p>
</div>
</div>

View file

@ -1,30 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.values.controller">
<label>Values</label>
<div
ng-repeat="value in agg.params.values track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.values[$index]"
values-list="agg.params.values"
values-list-min="0"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<p ng-show="aggForm.validLength.$invalid" class="text-danger text-center">
You must specify at least one value
</p>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add value
</button>
</div>

View file

@ -5,8 +5,9 @@ define(function (require) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
require('components/agg_types/controls/_values_list');
var valuesEditor = require('text!components/agg_types/controls/values.html');
var valuesEditor = require('text!components/agg_types/controls/percentile_ranks.html');
// required by the values editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,20 +29,7 @@ define(function (require) {
{
name: 'values',
editor: valuesEditor,
default: [],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.values.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.values.push(_.last($scope.agg.params.values) + 1);
};
$scope.$watchCollection('agg.params.values', function (values) {
$scope.validLength = _.size(values) || null;
});
}
default: []
}
],
getResponseAggs: function (agg) {
@ -60,4 +48,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -6,8 +6,9 @@ define(function (require) {
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var ordinalSuffix = require('utils/ordinal_suffix');
require('components/agg_types/controls/_values_list');
var percentEditor = require('text!components/agg_types/controls/percents.html');
var percentsEditor = require('text!components/agg_types/controls/percentiles.html');
// required by the percentiles editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,21 +29,8 @@ define(function (require) {
},
{
name: 'percents',
editor: percentEditor,
default: [1, 5, 25, 50, 75, 95, 99],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.percents.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.percents.push(_.last($scope.agg.params.percents) + 1);
};
$scope.$watchCollection('agg.params.percents', function (percents) {
$scope.validLength = _.size(percents) || null;
});
}
editor: percentsEditor,
default: [1, 5, 25, 50, 75, 95, 99]
}
],
getResponseAggs: function (agg) {
@ -61,4 +49,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -0,0 +1,26 @@
define(function (require) {
var _ = require('lodash');
require('modules')
.get('kibana')
.filter('commaList', function () {
/**
* Angular filter that accepts either an array or a comma-seperated string
* and outputs either an array, or a comma-seperated string for presentation.
*
* @param {String|Array} input - The comma-seperated list or array
* @param {Boolean} inclusive - Should the list be joined with an "and"?
* @return {String}
*/
return function (input, inclusive) {
var list = _.commaSeperatedList(input);
if (list.length < 2) {
return list.join('');
}
var conj = inclusive ? ' and ' : ' or ';
return list.slice(0, -1).join(', ') + conj + _.last(list);
};
});
});

View file

@ -45,7 +45,7 @@ define(function (require) {
queue.forEach(function (q) { q.reject(err); });
})
.finally(function () {
$rootScope.$emit('change:config', updated.concat(deleted));
$rootScope.$broadcast('change:config', updated.concat(deleted));
});
};
@ -70,7 +70,7 @@ define(function (require) {
var defer = Promise.defer();
queue.push(defer);
notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal);
$rootScope.$emit('change:config.' + key, newVal, oldVal);
$rootScope.$broadcast('change:config.' + key, newVal, oldVal);
// reset the fire timer
clearTimeout(timer);
@ -80,4 +80,4 @@ define(function (require) {
};
};
});
});

View file

@ -19,7 +19,7 @@ define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var defaults = require('components/config/defaults');
var defaults = Private(require('components/config/defaults'));
var DelayedUpdater = Private(require('components/config/_delayed_updater'));
var vals = Private(require('components/config/_vals'));
@ -123,6 +123,29 @@ define(function (require) {
if (updater) updater.fire();
};
/**
* A little helper for binding config variables to $scopes
*
* @param {Scope} $scope - an angular $scope object
* @param {string} key - the config key to bind to
* @param {string} [property] - optional property name where the value should
* be stored. Defaults to the config key
* @return {function} - an unbind function
*/
config.$bind = function ($scope, key, property) {
if (!property) property = key;
var update = function () {
$scope[property] = config.get(key);
};
update();
return _.partial(_.invoke, [
$scope.$on('change:config.' + key, update),
$scope.$on('init:config', update)
], 'call');
};
/*****
* PRIVATE API
*****/

View file

@ -1,84 +1,93 @@
define(function (require) {
var _ = require('lodash');
return function () {
var _ = require('lodash');
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 6,
description: 'The maximum geoHash size allowed in a tile map',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
}
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 7,
description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' +
'12 is the max. Explanation of cell dimensions: http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
},
'indexPattern:fieldMapping:lookBack': {
value: 5,
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
'patterns from which to query the field mapping.'
}
};
};
});
});

View file

@ -43,6 +43,7 @@ define(function (require) {
// the id of the document
self.id = config.id || void 0;
self.defaults = config.defaults;
/**
* Asynchronously initialize this object - will only run
@ -119,20 +120,7 @@ define(function (require) {
_.assign(self, self._source);
return Promise.try(function () {
// if we have a searchSource, set it's state based on the searchSourceJSON field
if (self.searchSource) {
var state = {};
try {
state = JSON.parse(meta.searchSourceJSON);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
parseSearchSource(meta.searchSourceJSON);
})
.then(hydrateIndexPattern)
.then(function () {
@ -153,6 +141,23 @@ define(function (require) {
});
});
function parseSearchSource(searchSourceJson) {
if (!self.searchSource) return;
// if we have a searchSource, set its state based on the searchSourceJSON field
var state = {};
try {
state = JSON.parse(searchSourceJson);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
/**
* After creation or fetching from ES, ensure that the searchSources index indexPattern
* is an bonafide IndexPattern object.
@ -181,14 +186,12 @@ define(function (require) {
});
}
/**
* Save this object
* Serialize this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
* @return {Object}
*/
self.save = function () {
self.serialize = function () {
var body = {};
_.forOwn(mapping, function (fieldMapping, fieldName) {
@ -205,6 +208,18 @@ define(function (require) {
};
}
return body;
};
/**
* Save this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
*/
self.save = function () {
var body = self.serialize();
// Slugify the object id
self.id = slugifyId(self.id);
@ -229,11 +244,11 @@ define(function (require) {
return docSource.doCreate(source)
.then(finish)
.catch(function (err) {
var confirmMessage = 'Are you sure you want to overwrite this?';
var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?';
if (_.deepGet(err, 'origError.status') === 409 && window.confirm(confirmMessage)) {
return docSource.doIndex(source).then(finish);
}
return Promise.resolve(false);
return Promise.reject(err);
});
};
@ -264,7 +279,6 @@ define(function (require) {
});
});
};
}
return SavedObject;

View file

@ -1,13 +1,14 @@
<th width="1%"></th>
<th ng-if="indexPattern.timeFieldName">
<span ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time">Time <i ng-class="headerClass(indexPattern.timeFieldName)"></i></span>
<span>Time <i ng-class="headerClass(indexPattern.timeFieldName)" ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time"></i></span>
</th>
<th ng-repeat="name in columns">
<span ng-click="sort(name)" class="table-header-name" tooltip="{{tooltip(name)}}">
{{name | shortDots}} <i ng-class="headerClass(name)"></i>
<span class="table-header-name">
{{name | shortDots}} <i ng-class="headerClass(name)" ng-click="sort(name)" tooltip="{{tooltip(name)}}" tooltip-append-to-body="1"></i>
</span>
<span class="table-header-move">
<i ng-click="moveLeft(name)" class="fa fa-angle-double-left" ng-show="!$first" tooltip="Move column to the left"></i>
<i ng-click="moveRight(name)" class="fa fa-angle-double-right" ng-show="!$last" tooltip="Move column to the right"></i>
<i ng-click="toggleColumn(name)" ng-show="canRemove(name)" class="fa fa-remove" tooltip="Remove column" tooltip-append-to-body="1"></i>
<i ng-click="moveLeft(name)" class="fa fa-angle-double-left" ng-show="!$first" tooltip="Move column to the left" tooltip-append-to-body="1"></i>
<i ng-click="moveRight(name)" class="fa fa-angle-double-right" ng-show="!$last" tooltip="Move column to the right" tooltip-append-to-body="1"></i>
</span>
</th>
</th>

View file

@ -18,13 +18,18 @@ define(function (require) {
var sortableField = function (field) {
if (!$scope.indexPattern) return;
return $scope.indexPattern.fields.byName[field].sortable;
var sortable = _.deepGet($scope.indexPattern.fields.byName[field], 'sortable');
return sortable;
};
$scope.tooltip = function (column) {
if (!sortableField(column)) return ''; else return 'Sort by ' + shortDotsFilter(column);
};
$scope.canRemove = function (name) {
return (name !== '_source' || $scope.columns.length !== 1);
};
$scope.headerClass = function (column) {
if (!sortableField(column)) return;
@ -49,6 +54,10 @@ define(function (require) {
_.move($scope.columns, index, ++index);
};
$scope.toggleColumn = function (fieldName) {
_.toggleInOut($scope.columns, fieldName);
};
$scope.sort = function (column) {
if (!column || !sortableField(column)) return;

View file

@ -12,7 +12,7 @@ define(function (require) {
require('filters/short_dots');
// guestimate at the minimum number of chars wide cells in the table should be
// guesstimate at the minimum number of chars wide cells in the table should be
var MIN_LINE_LENGTH = 20;
/**
@ -226,7 +226,7 @@ define(function (require) {
* Create the $$_formatted key on a row
*/
function _formatRow(row) {
row.$$_flattened = row.$$_flattened || $scope.indexPattern.flattenHit(row);
$scope.indexPattern.flattenHit(row);
row.$$_formatted = row.$$_formatted || _.mapValues(row.$$_flattened, _formatField);
return row.$$_formatted;
}
@ -235,4 +235,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -2,5 +2,5 @@
<a class="pull-right" ng-href="#/doc/{{indexPattern.id}}/{{row._index}}/{{row._type}}/?id={{row._id | uriescape}}">
<small>Link to /{{row._index}}/{{row._type}}/{{row._id | uriescape}}</small></i>
</a>
<doc-viewer hit="row" filter="filter" index-pattern="indexPattern"></doc-viewer>
<doc-viewer hit="row" filter="filter" columns="columns" index-pattern="indexPattern"></doc-viewer>
</td>

View file

@ -1,4 +1,7 @@
<div ng-if="hits.length">
<div class="spinner large" ng-show="searchSource.activeFetchCount > 0"></div>
<div
ng-if="hits.length"
ng-class="{ loading: searchSource.activeFetchCount > 0 }">
<paginate ng-if="!infiniteScroll" list="hits" per-page="50" top-controls="true">
<table class="kbn-table table" ng-if="indexPattern">
<thead

View file

@ -11,7 +11,7 @@ define(function (require) {
require('components/doc_table/components/table_row');
require('modules').get('kibana')
.directive('docTable', function (config, Notifier) {
.directive('docTable', function (config, Notifier, getAppState) {
return {
restrict: 'E',
template: html,
@ -55,6 +55,24 @@ define(function (require) {
$scope.limit += 50;
};
// This exists to fix the problem of an empty initial column list not playing nice with watchCollection.
$scope.$watch('columns', function (columns) {
if (columns.length !== 0) return;
var $state = getAppState();
$scope.columns.push('_source');
if ($state) $state.replace();
});
$scope.$watchCollection('columns', function (columns, oldColumns) {
if (oldColumns.length === 1 && oldColumns[0] === '_source' && $scope.columns.length > 1) {
_.pull($scope.columns, '_source');
}
if ($scope.columns.length === 0) $scope.columns.push('_source');
});
$scope.$watch('searchSource', prereq(function (searchSource) {
if (!$scope.searchSource) return;

View file

@ -6,4 +6,17 @@ doc-table {
overflow: auto;
margin: 5px;
.flex(1, 1, 100%);
.loading {
opacity: @loading-opacity;
}
.spinner {
position: absolute;
top: 40%;
left: 0;
right: 0;
z-index: 20;
opacity: @loading-opacity;
}
}

View file

@ -5,46 +5,56 @@
</ul>
<div class="content">
<table class="table table-condensed" ng-show="mode == 'table'" bindonce>
<table class="table table-condensed" ng-show="mode == 'table'">
<tbody>
<tr ng-repeat="field in fields" bindonce>
<tr ng-repeat="field in fields">
<td field-name="field"
field-type="mapping[field].type"
width="1%"
class="doc-viewer-field">
</td>
<td width="1%" class="doc-viewer-buttons" ng-if="filter">
<span bo-if="mapping[field].filterable">
<i ng-click="filter(mapping[field], flattened[field], '+')" class="fa fa-search-plus"></i>
<i ng-click="filter(mapping[field], flattened[field],'-')" class="fa fa-search-minus"></i>
<span ng-if="mapping[field].filterable">
<i ng-click="filter(mapping[field], flattened[field], '+')"
tooltip="Filter for value"
tooltip-append-to-body="1"
class="fa fa-search-plus"></i>
<i ng-click="filter(mapping[field], flattened[field],'-')"
tooltip="Filter out value"
tooltip-append-to-body="1"
class="fa fa-search-minus"></i>
</span>
<span bo-if="!mapping[field].filterable" tooltip="Unindexed fields can not be searched">
<span ng-if="!mapping[field].filterable" tooltip="Unindexed fields can not be searched">
<i class="fa fa-search-plus text-muted"></i>
<i class="fa fa-search-minus text-muted"></i>
</span>
<span ng-if="columns">
<i ng-click="toggleColumn(field)"
tooltip="Toggle column in table"
tooltip-append-to-body="1"
class="fa fa-columns"></i>
</span>
</td>
<td>
<i bo-if="!mapping[field] && field[0] === '_'"
<i ng-if="!mapping[field] && field[0] === '_'"
tooltip-placement="top"
tooltip="Field names beginning with _ are not supported"
class="fa fa-warning text-color-warning ng-scope doc-viewer-underscore"></i>
<i bo-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
<i ng-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="No cached mapping for this field. Refresh your mapping from the Settings > Indices page"
class="fa fa-warning text-color-warning ng-scope doc-viewer-no-mapping"></i>
<i bo-if="showArrayInObjectsWarning(doc, field)"
<i ng-if="showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="Objects in arrays are not well supported."
class="fa fa-warning text-color-warning ng-scope doc-viewer-object-array"></i>
<span class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></span>
<div class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>
</table>
<pre ng-show="mode == 'json'">{{hit | json}}</pre>
<div id="json-ace" ng-show="mode == 'json'" readonly ui-ace="{ useWrapMode: true, advanced: { highlightActiveLine: false }, rendererOptions: { showPrintMargin: false, maxLines: 4294967296 }, mode: 'json' }" ng-model="hit_json"></div>
</div>
</div>

View file

@ -1,5 +1,7 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
require('angular-ui-ace');
var html = require('text!components/doc_viewer/doc_viewer.html');
require('css!components/doc_viewer/doc_viewer.css');
@ -15,6 +17,7 @@ define(function (require) {
hit: '=',
indexPattern: '=',
filter: '=?',
columns: '=?'
},
link: function ($scope, $el, attr) {
// If a field isn't in the mapping, use this
@ -24,6 +27,7 @@ define(function (require) {
$scope.mapping = $scope.indexPattern.fields.byName;
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
$scope.hit_json = angular.toJson($scope.hit, true);
$scope.formatted = _.mapValues($scope.flattened, function (value, name) {
var mapping = $scope.mapping[name];
var formatter = (mapping && mapping.format) ? mapping.format : defaultFormat;
@ -34,6 +38,10 @@ define(function (require) {
});
$scope.fields = _.keys($scope.flattened).sort();
$scope.toggleColumn = function (fieldName) {
_.toggleInOut($scope.columns, fieldName);
};
$scope.showArrayInObjectsWarning = function (row, field) {
var value = $scope.flattened[field];
return _.isArray(value) && typeof value[0] === 'object';

View file

@ -10,17 +10,69 @@ define(function (require) {
function KbnFormController($scope, $element) {
var self = this;
self.errorCount = function () {
return _.reduce(self.$error, function (count, controls, errorType) {
return count + _.size(controls);
}, 0);
self.errorCount = function (predicate) {
return self.$$invalidModels().length;
};
// same as error count, but filters out untouched and pristine models
self.softErrorCount = function () {
return self.$$invalidModels(function (model) {
return model.$touched || model.$dirty;
}).length;
};
self.describeErrors = function () {
var count = self.errorCount();
var count = self.softErrorCount();
return count + ' Error' + (count === 1 ? '' : 's');
};
self.$$invalidModels = function (predicate) {
predicate = _.createCallback(predicate);
var invalid = [];
_.forOwn(self.$error, function collect(models) {
if (!models) return;
models.forEach(function (model) {
if (model.$$invalidModels) {
// recurse into child form
_.forOwn(model.$error, collect);
} else {
if (predicate(model)) {
// prevent dups
var len = invalid.length;
while (len--) if (invalid[len] === model) return;
invalid.push(model);
}
}
});
});
return invalid;
};
self.$setTouched = function () {
self.$$invalidModels().forEach(function (model) {
// only kbnModels have $setTouched
if (model.$setTouched) model.$setTouched();
});
};
function filterSubmits(event) {
if (self.errorCount()) {
event.preventDefault();
event.stopImmediatePropagation();
self.$setTouched();
}
}
$element.on('submit', filterSubmits);
$scope.$on('$destroy', function () {
$element.off('submit', filterSubmits);
});
}
return KbnFormController;
});
});

View file

@ -3,6 +3,8 @@ define(function (require) {
var angular = require('angular');
var PRISTINE_CLASS = 'ng-pristine';
var DIRTY_CLASS = 'ng-dirty';
var UNTOUCHED_CLASS = 'ng-untouched';
var TOUCHED_CLASS = 'ng-touched';
// http://goo.gl/eJofve
var nullFormCtrl = {
@ -43,13 +45,32 @@ define(function (require) {
* @return {undefined}
*/
ngModel.$setDirty = function () {
ngModel.$setTouched();
$$setDirty();
};
function $$setDirty() {
if (ngModel.$dirty) return;
ngModel.$dirty = true;
ngModel.$pristine = false;
$animate.removeClass($element, PRISTINE_CLASS);
$animate.addClass($element, DIRTY_CLASS);
ngModel.$getForm().$setDirty();
};
}
ngModel.$setTouched = toggleTouched(true);
ngModel.$setUntouched = toggleTouched(false);
function toggleTouched(val) {
return function () {
if (ngModel.$touched === val) return;
ngModel.$touched = val;
ngModel.$untouched = !val;
$animate.addClass($element, val ? TOUCHED_CLASS : UNTOUCHED_CLASS);
$animate.removeClass($element, val ? UNTOUCHED_CLASS : TOUCHED_CLASS);
};
}
/**
* While the model is pristine, ensure that the model
@ -70,7 +91,7 @@ define(function (require) {
if (is === was) return;
unwatch();
waitForPristine();
ngModel.$setDirty();
$$setDirty();
}
}
@ -94,9 +115,21 @@ define(function (require) {
};
}
if (ngModel.$dirty) waitForPristine();
else watchForDirtyOrInvalid();
ngModel.$setUntouched();
$element.one('blur', function () {
ngModel.$setTouched();
$scope.$apply();
});
$scope.$on('$destroy', function () {
$element.off('blur', ngModel.$setTouched);
});
// wait for child scope to init before watching validity
$scope.$evalAsync(function () {
if (ngModel.$dirty) waitForPristine();
else watchForDirtyOrInvalid();
});
}
return KbnModelController;
});
});

View file

@ -26,6 +26,7 @@ define(function (require) {
Private(require('./mapExists')),
Private(require('./mapMissing')),
Private(require('./mapQueryString')),
Private(require('./mapGeoBoundingBox')),
Private(require('./mapScript')),
Private(require('./mapDefault')) // ProTip: last one to get applied
];

View file

@ -0,0 +1,21 @@
define(function (require) {
var _ = require('lodash');
return function mapGeoBoundBoxProvider(Promise, courier) {
return function (filter) {
var key, value, topLeft, bottomRight, field;
if (filter.geo_bounding_box) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
key = _.keys(filter.geo_bounding_box)[0];
field = indexPattern.fields.byName[key];
topLeft = field.format.convert(filter.geo_bounding_box[field.name].top_left);
bottomRight = field.format.convert(filter.geo_bounding_box[field.name].bottom_right);
value = topLeft + ' to ' + bottomRight;
return { key: key, value: value };
});
}
return Promise.reject(filter);
};
};
});

View file

@ -0,0 +1,19 @@
define(function (require) {
var _ = require('lodash');
return function () {
return function ($state) {
if (!_.isObject($state)) throw new Error ('pushFilters requires a state object');
return function (filter, negate, index) {
// Hierarchical and tabular data set their aggConfigResult parameter
// differently because of how the point is rewritten between the two. So
// we need to check if the point.orig is set, if not use try the point.aggConfigResult
var filters = _.clone($state.filters || []);
var pendingFilter = { meta: { negate: negate, index: index }};
_.extend(pendingFilter, filter);
filters.push(pendingFilter);
$state.filters = filters;
};
};
};
});

View file

@ -21,6 +21,7 @@ define(function (require) {
{ name: 'geo_shape', type: 'geo_shape', group: 'geo' },
{ name: 'ip', type: 'ip', group: 'other' },
{ name: 'attachment', type: 'attachment', group: 'other' },
{ name: 'murmur3', type: 'murmur3', group: 'hash' }
]
});

View file

@ -102,7 +102,8 @@ define(function (require) {
},
{
types: [
'number'
'number',
'murmur3'
],
name: 'number',
convert: function (val) {
@ -140,6 +141,7 @@ define(function (require) {
formats.defaultByType = {
number: formats.byName.number,
murmur3: formats.byName.number,
date: formats.byName.date,
boolean: formats.byName.string,
ip: formats.byName.ip,

View file

@ -16,6 +16,7 @@ define(function (require) {
{ name: 'geo_point', sortable: false, filterable: false },
{ name: 'geo_shape', sortable: false, filterable: false },
{ name: 'attachment', sortable: false, filterable: false },
{ name: 'murmur3', sortable: false, filterable: false }
]
});
};

View file

@ -1,21 +1,61 @@
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a flattened version
define(function (require) {
var _ = require('lodash');
return function (hit) {
if (hit.$$_flattened) return hit.$$_flattened;
return function FlattenHitProvider(config, $rootScope) {
var self = this;
var source = self.flattenSearchResponse(hit._source);
var fields = _.omit(self.flattenSearchResponse(hit.fields), function (val, name) {
var field = self.fields.byName[name];
if (field && !field.scripted && !_.has(source, name)) {
return true;
} else {
return false;
}
var _ = require('lodash');
var metaFields = config.get('metaFields');
$rootScope.$on('change:config.metaFields', function () {
metaFields = config.get('metaFields');
});
return hit.$$_flattened = _.merge(source, fields, _.pick(hit, self.metaFields), _.pick(hit.fields, self.metaFields));
function flattenHit(indexPattern, hit) {
var flat = {};
// recursively merge _source
var fields = indexPattern.fields.byName;
(function flatten(obj, keyPrefix) {
keyPrefix = keyPrefix ? keyPrefix + '.' : '';
_.forOwn(obj, function (val, key) {
key = keyPrefix + key;
if (flat[key] !== void 0) return;
var hasValidMapping = (fields[key] && fields[key].type !== 'conflict');
var isValue = !_.isPlainObject(val);
if (hasValidMapping || isValue) {
flat[key] = val;
return;
}
flatten(val, key);
});
}(hit._source));
// assign the meta fields
_.each(metaFields, function (meta) {
if (meta === '_source') return;
flat[meta] = hit[meta];
});
// unwrap computed fields
_.forOwn(hit.fields, function (val, key) {
if (key[0] === '_' && !_.contains(metaFields, key)) return;
flat[key] = _.isArray(val) && val.length === 1 ? val[0] : val;
});
return flat;
}
function cachedFlatten(indexPattern, hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
}
cachedFlatten.uncached = flattenHit;
return cachedFlatten;
};
});

View file

@ -1,24 +0,0 @@
define(function (require) {
var _ = require('lodash');
return function (nestedObj) {
var key; // original key
var stack = []; // track key stack
var flatObj = {};
var self = this;
(function flattenObj(obj) {
_.keys(obj).forEach(function (key) {
stack.push(key);
var flattenKey = stack.join('.');
if ((self.fields.byName[flattenKey] || _.isArray(obj[key]) || !_.isObject(obj[key]))) {
flatObj[flattenKey] = obj[key];
} else if (_.isObject(obj[key])) {
flattenObj(obj[key]);
}
stack.pop();
});
}(nestedObj));
return flatObj;
};
});

View file

@ -1,5 +1,5 @@
define(function (require) {
return function IndexPatternFactory(Private, timefilter, configFile, Notifier, shortDotsFilter, config, Promise) {
return function IndexPatternFactory(Private, timefilter, Notifier, config, Promise) {
var _ = require('lodash');
var angular = require('angular');
var errors = require('errors');
@ -9,9 +9,9 @@ define(function (require) {
var fieldFormats = Private(require('components/index_patterns/_field_formats'));
var intervals = Private(require('components/index_patterns/_intervals'));
var fieldTypes = Private(require('components/index_patterns/_field_types'));
var flattenSearchResponse = require('components/index_patterns/_flatten_search_response');
var flattenHit = require('components/index_patterns/_flatten_hit');
var flattenHit = Private(require('components/index_patterns/_flatten_hit'));
var getComputedFields = require('components/index_patterns/_get_computed_fields');
var shortDotsFilter = Private(require('filters/short_dots'));
var DocSource = Private(require('components/courier/data_source/doc_source'));
@ -43,7 +43,7 @@ define(function (require) {
self.init = function () {
// tell the docSource where to find the doc
docSource
.index(configFile.kibana_index)
.index(config.file.kibana_index)
.type(type)
.id(self.id);
@ -104,7 +104,7 @@ define(function (require) {
}
},
filterable: {
value: field.name === '_id' || ((field.indexed && type.filterable) || field.scripted)
value: field.name === '_id' || ((field.indexed && type && type.filterable) || field.scripted)
},
format: {
get: function () {
@ -113,7 +113,7 @@ define(function (require) {
}
},
sortable: {
value: field.indexed && type.sortable
value: field.indexed && type && type.sortable
},
scripted: {
// enumerable properties end up in the JSON
@ -275,13 +275,10 @@ define(function (require) {
return '' + self.toJSON();
};
self.metaFields = config.get('metaFields');
self.flattenSearchResponse = flattenSearchResponse.bind(self);
self.flattenHit = flattenHit.bind(self);
self.flattenHit = _.partial(flattenHit, self);
self.getComputedFields = getComputedFields.bind(self);
}
return IndexPattern;
};
});

View file

@ -1,5 +1,5 @@
define(function (require) {
return function MapperService(Private, Promise, es, configFile) {
return function MapperService(Private, Promise, es, config) {
var _ = require('lodash');
var moment = require('moment');
@ -33,7 +33,7 @@ define(function (require) {
if (!skipIndexPatternCache) {
return es.get({
index: configFile.kibana_index,
index: config.file.kibana_index,
type: 'index-pattern',
id: id,
_sourceInclude: ['fields']
@ -51,7 +51,7 @@ define(function (require) {
promise = self.getIndicesForIndexPattern(indexPattern)
.then(function (existing) {
if (existing.matches.length === 0) throw new IndexPatternMissingIndices();
return existing.matches.slice(-5); // Grab the most recent 5
return existing.matches.slice(-config.get('indexPattern:fieldMapping:lookBack')); // Grab the most recent
});
}

View file

@ -1,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var nextTick = require('utils/next_tick');
var $ = require('jquery');
var modules = require('modules');
var module = modules.get('kibana/notify');

View file

@ -0,0 +1,34 @@
<div
ng-repeat="value in numberListCntr.getList() track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="numberListCntr.getList()[$index]"
kbn-number-list-input
input-focus
class="form-control">
<button
ng-click="numberListCntr.remove($index, 1)"
class="btn btn-danger btn-xs"
type="button">
<i class="fa fa-times"></i>
</button>
</div>
<p ng-show="numberListCntr.invalidLength()" class="text-danger text-center">
You must specify at least one {{numberListCntr.getUnitName()}}
</p>
<p ng-show="numberListCntr.undefinedLength()" class="text-primary text-center">
<!-- be a bit more polite when the form is first init'd -->
Please specify at least one {{numberListCntr.getUnitName()}}
</p>
<button
ng-click="numberListCntr.add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add {{numberListCntr.getUnitName()}}
</button>

View file

@ -0,0 +1,108 @@
define(function (require) {
var _ = require('lodash');
var parseRange = require('utils/range');
require('components/number_list/number_list_input');
require('modules')
.get('kibana')
.directive('kbnNumberList', function () {
return {
restrict: 'E',
template: require('text!components/number_list/number_list.html'),
controllerAs: 'numberListCntr',
require: 'ngModel',
controller: function ($scope, $attrs, $parse) {
var self = this;
// Called from the pre-link function once we have the controllers
self.init = function (modelCntr) {
self.modelCntr = modelCntr;
self.getList = function () {
return self.modelCntr.$modelValue;
};
self.getUnitName = _.partial($parse($attrs.unit), $scope);
var defaultRange = self.range = parseRange('[0,Infinity)');
$scope.$watch(function () {
return $attrs.range;
}, function (range, prev) {
if (!range) {
self.range = defaultRange;
return;
}
try {
self.range = parseRange(range);
} catch (e) {
throw new TypeError('Unable to parse range: ' + e.message);
}
});
/**
* Remove an item from list by index
* @param {number} index
* @return {undefined}
*/
self.remove = function (index) {
var list = self.getList();
if (!list) return;
list.splice(index, 1);
};
/**
* Add an item to the end of the list
* @return {undefined}
*/
self.add = function () {
var list = self.getList();
if (!list) return;
list.push(_.last(list) + 1);
};
/**
* Check to see if the list is too short.
*
* @return {Boolean}
*/
self.tooShort = function () {
return _.size(self.getList()) < 1;
};
/**
* Check to see if the list is too short, but simply
* because the user hasn't interacted with it yet
*
* @return {Boolean}
*/
self.undefinedLength = function () {
return self.tooShort() && (self.modelCntr.$untouched && self.modelCntr.$pristine);
};
/**
* Check to see if the list is too short
*
* @return {Boolean}
*/
self.invalidLength = function () {
return self.tooShort() && !self.undefinedLength();
};
$scope.$watchCollection(self.getList, function () {
self.modelCntr.$setValidity('numberListLength', !self.tooShort());
});
};
},
link: {
pre: function ($scope, $el, attrs, ngModelCntr) {
$scope.numberListCntr.init(ngModelCntr);
}
},
};
});
});

View file

@ -6,18 +6,21 @@ define(function (require) {
var INVALID = {}; // invalid flag
var FLOATABLE = /^[\d\.e\-\+]+$/i;
var VALIDATION_ERROR = 'numberListRangeAndOrder';
var DIRECTIVE_ATTR = 'kbn-number-list-input';
require('modules')
.get('kibana')
.directive('valuesList', function ($parse) {
.directive('kbnNumberListInput', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, attrs, ngModelController) {
require: ['ngModel', '^kbnNumberList'],
link: function ($scope, $el, attrs, controllers) {
var ngModelCntr = controllers[0];
var numberListCntr = controllers[1];
var $setModel = $parse(attrs.ngModel).assign;
var $repeater = $el.closest('[ng-repeat]');
var $listGetter = $parse(attrs.valuesList);
var $minValue = $parse(attrs.valuesListMin);
var $maxValue = $parse(attrs.valuesListMax);
var handlers = {
up: change(add, 1),
@ -29,14 +32,16 @@ define(function (require) {
tab: go('next'),
'shift-tab': go('prev'),
'shift-enter': numberListCntr.add,
backspace: removeIfEmpty,
delete: removeIfEmpty
};
function removeIfEmpty(event) {
if ($el.val() === '') {
if (!ngModelCntr.$viewValue) {
$get('prev').focus();
$scope.remove($scope.$index);
numberListCntr.remove($scope.$index);
event.preventDefault();
}
@ -44,7 +49,7 @@ define(function (require) {
}
function $get(dir) {
return $repeater[dir]().find('[values-list]');
return $repeater[dir]().find('[' + DIRECTIVE_ATTR + ']');
}
function go(dir) {
@ -88,7 +93,7 @@ define(function (require) {
function change(using, mod) {
return function () {
var str = String(ngModelController.$viewValue);
var str = String(ngModelCntr.$viewValue);
var val = parse(str);
if (val === INVALID) return;
@ -96,7 +101,7 @@ define(function (require) {
if (next === INVALID) return;
$el.val(next);
ngModelController.$setViewValue(next);
ngModelCntr.$setViewValue(next);
};
}
@ -117,17 +122,26 @@ define(function (require) {
});
function parse(viewValue) {
viewValue = String(viewValue || 0);
var num = viewValue.trim();
if (!FLOATABLE.test(num)) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
var num = viewValue;
var list = $listGetter($scope);
var min = list[$scope.$index - 1] || $minValue($scope);
var max = list[$scope.$index + 1] || $maxValue($scope);
if (typeof num !== 'number' || isNaN(num)) {
// parse non-numbers
num = String(viewValue || 0).trim();
if (!FLOATABLE.test(num)) return INVALID;
if (num <= min || num >= max) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
}
var range = numberListCntr.range;
if (!range.within(num)) return INVALID;
if ($scope.$index > 0) {
var i = $scope.$index - 1;
var list = numberListCntr.getList();
var prev = list[i];
if (num <= prev) return INVALID;
}
return num;
}
@ -137,31 +151,35 @@ define(function (require) {
{
fn: $scope.$watchCollection,
get: function () {
return $listGetter($scope);
return numberListCntr.getList();
}
}
], function () {
var valid = parse(ngModelController.$viewValue) !== INVALID;
ngModelController.$setValidity('valuesList', valid);
var valid = parse(ngModelCntr.$viewValue) !== INVALID;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
});
function validate(then) {
return function (input) {
var value = parse(input);
var valid = value !== INVALID;
value = valid ? value : void 0;
ngModelController.$setValidity('valuesList', valid);
value = valid ? value : input;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
then && then(input, value);
return value;
};
}
ngModelController.$parsers.push(validate());
ngModelController.$formatters.push(validate(function (input, value) {
ngModelCntr.$parsers.push(validate());
ngModelCntr.$formatters.push(validate(function (input, value) {
if (input !== value) $setModel($scope, value);
}));
if (parse(ngModelCntr.$viewValue) === INVALID) {
ngModelCntr.$setTouched();
}
}
};
});
});
});

View file

@ -1,19 +1,23 @@
define(function (require) {
var _ = require('lodash');
var modules = require('modules');
var urlParam = '_a';
function AppStateProvider(Private, $rootScope, getAppState) {
var State = Private(require('components/state_management/state'));
_(AppState).inherits(State);
function AppState(defaults) {
AppState.Super.call(this, '_a', defaults);
AppState.Super.call(this, urlParam, defaults);
getAppState._set(this);
}
// if the url param is missing, write it back
AppState.prototype._persistAcrossApps = false;
AppState.prototype.destroy = function () {
AppState.Super.prototype.destroy.call(this);
getAppState._set(null);
@ -26,13 +30,19 @@ define(function (require) {
.factory('AppState', function (Private) {
return Private(AppStateProvider);
})
.service('getAppState', function () {
.service('getAppState', function ($location) {
var currentAppState;
function get() {
return currentAppState;
}
// Checks to see if the appState might already exist, even if it hasn't been newed up
get.previouslyStored = function () {
var search = $location.search();
return search[urlParam] ? true : false;
};
get._set = function (current) {
currentAppState = current;
};

View file

@ -24,7 +24,6 @@ define(function (require) {
controller: function ($scope) {
var init = function () {
$scope.setMode($scope.mode);
$scope.formatRelative();
};
$scope.format = 'MMMM Do YYYY, HH:mm:ss.SSS';
@ -96,6 +95,7 @@ define(function (require) {
}
if ($scope.from.toString().split('/')[1]) $scope.relative.round = true;
$scope.formatRelative();
break;
case 'absolute':

View file

@ -43,7 +43,8 @@ define(function (require) {
if (self.shouldAutoReload(next, prev)) {
var appState = getAppState();
appState.destroy();
if (appState) appState.destroy();
reloading = $rootScope.$on('$locationChangeSuccess', function () {
// call the "unlisten" function returned by $on
reloading();

View file

@ -182,7 +182,7 @@ define(function (require) {
*/
AggConfig.prototype.requesting = function () {
var self = this;
self.type.params.forEach(function (param) {
self.type && self.type.params.forEach(function (param) {
if (param.onRequest) param.onRequest(self);
});
};

View file

@ -102,6 +102,14 @@ define(function (require) {
}
};
Vis.prototype.hasSchemaAgg = function (schemaName, aggTypeName) {
var aggs = this.aggs.bySchemaName[schemaName] || [];
return aggs.some(function (agg) {
if (!agg.type || !agg.type.name) return false;
return agg.type.name === aggTypeName;
});
};
return Vis;
};
});

Some files were not shown because too many files have changed in this diff Show more