mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Merge] Merged with latest hapi server
This commit is contained in:
parent
3773894e71
commit
409bc43baf
259 changed files with 13513 additions and 133510 deletions
|
@ -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
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"node": true,
|
||||
|
||||
"globals": {
|
||||
"Promise": true
|
||||
"Promise": true,
|
||||
"status": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/**/*'
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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[]
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
65
src/hapi/bin/kibana.js
Normal 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);
|
||||
});
|
|
@ -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);
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = function () {
|
||||
return require('../config');
|
||||
};
|
10
src/hapi/lib/config/check_path.js
Normal file
10
src/hapi/lib/config/check_path.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
var fs = require('fs');
|
||||
module.exports = function checkPath(path) {
|
||||
try {
|
||||
fs.statSync(path);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
81
src/hapi/lib/config/config.js
Normal file
81
src/hapi/lib/config/config.js
Normal 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;
|
19
src/hapi/lib/config/explode_by.js
Normal file
19
src/hapi/lib/config/explode_by.js
Normal 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;
|
||||
};
|
||||
|
||||
|
18
src/hapi/lib/config/flatten_with.js
Normal file
18
src/hapi/lib/config/flatten_with.js
Normal 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;
|
||||
};
|
||||
|
||||
|
6
src/hapi/lib/config/index.js
Normal file
6
src/hapi/lib/config/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
var Config = require('./config');
|
||||
var schema = require('./schema');
|
||||
var config = new Config(schema);
|
||||
module.exports = function () {
|
||||
return config;
|
||||
};
|
11
src/hapi/lib/config/override.js
Normal file
11
src/hapi/lib/config/override.js
Normal 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));
|
||||
};
|
||||
|
||||
|
71
src/hapi/lib/config/schema.js
Normal file
71
src/hapi/lib/config/schema.js
Normal 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();
|
||||
|
|
@ -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'));
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
});
|
||||
};
|
28
src/hapi/lib/load_settings_from_yaml.js
Normal file
28
src/hapi/lib/load_settings_from_yaml.js
Normal 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;
|
||||
};
|
||||
|
35
src/hapi/lib/logging/index.js
Normal file
35
src/hapi/lib/logging/index.js
Normal 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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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;
|
15
src/hapi/lib/plugins/add_statics_for_public.js
Normal file
15
src/hapi/lib/plugins/add_statics_for_public.js
Normal 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);
|
||||
};
|
24
src/hapi/lib/plugins/list_plugins.js
Normal file
24
src/hapi/lib/plugins/list_plugins.js
Normal 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;
|
||||
};
|
||||
|
21
src/hapi/lib/plugins/load_kibana_plugins.js
Normal file
21
src/hapi/lib/plugins/load_kibana_plugins.js
Normal 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);
|
||||
});
|
||||
};
|
18
src/hapi/lib/plugins/plugin.js
Normal file
18
src/hapi/lib/plugins/plugin.js
Normal 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;
|
22
src/hapi/lib/plugins/register_plugin_configs.js
Normal file
22
src/hapi/lib/plugins/register_plugin_configs.js
Normal 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;
|
||||
});
|
||||
};
|
|
@ -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();
|
|
@ -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;
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 () {
|
|
@ -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);
|
||||
};
|
||||
}
|
16
src/hapi/lib/write_pid_file.js
Normal file
16
src/hapi/lib/write_pid_file.js
Normal 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);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 ] }
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
});
|
||||
|
|
30
src/hapi/plugins/elasticsearch/lib/create_agent.js
Normal file
30
src/hapi/plugins/elasticsearch/lib/create_agent.js
Normal 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);
|
||||
};
|
19
src/hapi/plugins/elasticsearch/lib/create_proxy.js
Normal file
19
src/hapi/plugins/elasticsearch/lib/create_proxy.js
Normal 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);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
20
src/hapi/plugins/elasticsearch/lib/map_uri.js
Normal file
20
src/hapi/plugins/elasticsearch/lib/map_uri.js
Normal 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);
|
||||
};
|
||||
};
|
112
src/hapi/plugins/elasticsearch/lib/validate.js
Normal file
112
src/hapi/plugins/elasticsearch/lib/validate.js
Normal 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;
|
||||
|
|
@ -8,7 +8,7 @@ module.exports = new kibana.Plugin({
|
|||
path: '/{param*}',
|
||||
handler: {
|
||||
directory: {
|
||||
path: config.public_folder
|
||||
path: config.get('kibana.publicFolder')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
15
src/hapi/plugins/status/lib/series.js
Normal file
15
src/hapi/plugins/status/lib/series.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:{}})"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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) {
|
|||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
26
src/kibana/components/comma_list_filter.js
Normal file
26
src/kibana/components/comma_list_filter.js
Normal 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);
|
||||
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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) {
|
|||
};
|
||||
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*****/
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
|
21
src/kibana/components/filter_bar/lib/mapGeoBoundingBox.js
Normal file
21
src/kibana/components/filter_bar/lib/mapGeoBoundingBox.js
Normal 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);
|
||||
};
|
||||
};
|
||||
});
|
19
src/kibana/components/filter_bar/push_filter.js
Normal file
19
src/kibana/components/filter_bar/push_filter.js
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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' }
|
||||
]
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
34
src/kibana/components/number_list/number_list.html
Normal file
34
src/kibana/components/number_list/number_list.html
Normal 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>
|
108
src/kibana/components/number_list/number_list.js
Normal file
108
src/kibana/components/number_list/number_list.js
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue