Merge branch 'master' of github.com:elastic/kibana into metrics

This commit is contained in:
Chris Cowan 2017-01-02 09:54:40 -07:00
commit 2fb42d0e5d
258 changed files with 8418 additions and 1502 deletions

View file

@ -142,6 +142,8 @@ Start the development server.
> On Windows, you'll need you use Git Bash, Cygwin, or a similar shell that exposes the `sh` command. And to successfully build you'll need Cygwin optional packages zip, tar, and shasum.
Now you can point your web browser to https://localhost:5601 and start using Kibana! When running `npm start`, Kibana will also log that it is listening on port 5603 due to the base path proxy, but you should still access Kibana on port 5601.
#### Customizing `config/kibana.dev.yml`
The `config/kibana.yml` file stores user configuration directives. Since this file is checked into source control, however, developer preferences can't be saved without the risk of accidentally committing the modified version. To make customizing configuration easier during development, the Kibana CLI will look for a `config/kibana.dev.yml` file if run with the `--dev` flag. This file behaves just like the non-dev version and accepts any of the [standard settings](https://www.elastic.co/guide/en/kibana/master/kibana-server-properties.html).

View file

@ -33,6 +33,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea
* https://github.com/mstoyano/kbn_c3js_vis[C3JS Visualizations] (mstoyano)
* https://github.com/clamarque/Kibana_health_metric_vis[Health Metric] (clamarque)
* https://github.com/ommsolutions/kibana_ext_metrics_vis[Extended Metric] (ommsolutions)
* https://github.com/virusu/3D_kibana_charts_vis[3D Charts] (virusu)
[float]
=== Other

View file

@ -88,6 +88,10 @@ mappings are available:
`SERVER_PORT`:: `server.port`
`SERVER_SSL_CERT`:: `server.ssl.cert`
`SERVER_SSL_KEY`:: `server.ssl.key`
`XPACK_SECURITY_COOKIENAME`:: `xpack.security.cookieName`
`XPACK_SECURITY_ENCRYPTIONKEY`:: `xpack.security.encryptionKey`
`XPACK_SECURITY_SECURECOOKIES`:: `xpack.security.secureCookies`
`XPACK_SECURITY_SESSIONTIMEOUT`:: `xpack.security.sessionTimeout`
These variables can be set with +docker-compose+ like this:

View file

@ -121,3 +121,5 @@ include::visualize/tilemap.asciidoc[]
include::visualize/vertbar.asciidoc[]
include::visualize/tagcloud.asciidoc[]
include::visualize/heatmap.asciidoc[]

View file

@ -61,7 +61,7 @@ _silhouette_:: Displays each aggregation as variance from a central line.
Checkboxes are available to enable and disable the following behaviors:
*Smooth Lines*:: Check this box to curve the top boundary of the area from point to point.
*Line Mode*:: You can choose between straight line, smoothed line and stepped line.
*Set Y-Axis Extents*:: Check this box and enter values in the *y-max* and *y-min* fields to set the Y axis to specific
values.
*Scale Y-Axis to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check

View file

@ -0,0 +1,83 @@
[[heatmap-chart]]
== Heatmap Chart
A heat map is a graphical representation of data where the individual values contained in a matrix are represented as colors.
The color for each matrix position is determined by the _metrics_ aggregation. The following aggregations are available for
this chart:
include::y-axis-aggs.asciidoc[]
The _buckets_ aggregations determine what information is being retrieved from your data set.
Before you choose a buckets aggregation, specify if you are defining buckets for X or Y axis within a single chart
or splitting into multiple charts. A multiple chart split must run before any other aggregations.
When you split a chart, you can change if the splits are displayed in a row or a column by clicking
the *Rows | Columns* selector.
This chart's X and Y axis supports the following aggregations. Click the linked name of each aggregation to visit the main
Elasticsearch documentation for that aggregation.
*Date Histogram*:: A {es-ref}search-aggregations-bucket-datehistogram-aggregation.html[_date histogram_] is built from a
numeric field and organized by date. You can specify a time frame for the intervals in seconds, minutes, hours, days,
weeks, months, or years. You can also specify a custom interval frame by selecting *Custom* as the interval and
specifying a number and a time unit in the text field. Custom interval time units are *s* for seconds, *m* for minutes,
*h* for hours, *d* for days, *w* for weeks, and *y* for years. Different units support different levels of precision,
down to one second.
*Histogram*:: A standard {es-ref}search-aggregations-bucket-histogram-aggregation.html[_histogram_] is built from a
numeric field. Specify an integer interval for this field. Select the *Show empty buckets* checkbox to include empty
intervals in the histogram.
*Range*:: With a {es-ref}search-aggregations-bucket-range-aggregation.html[_range_] aggregation, you can specify ranges
of values for a numeric field. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to remove
a range.
*Date Range*:: A {es-ref}search-aggregations-bucket-daterange-aggregation.html[_date range_] aggregation reports values
that are within a range of dates that you specify. You can specify the ranges for the dates using
{es-ref}common-options.html#date-math[_date math_] expressions. Click *Add Range* to add a set of range endpoints.
Click the red *(x)* symbol to remove a range.
*IPv4 Range*:: The {es-ref}search-aggregations-bucket-iprange-aggregation.html[_IPv4 range_] aggregation enables you to
specify ranges of IPv4 addresses. Click *Add Range* to add a set of range endpoints. Click the red *(x)* symbol to
remove a range.
*Terms*:: A {es-ref}search-aggregations-bucket-terms-aggregation.html[_terms_] aggregation enables you to specify the top
or bottom _n_ elements of a given field to display, ordered by count or a custom metric.
*Filters*:: You can specify a set of {es-ref}search-aggregations-bucket-filters-aggregation.html[_filters_] for the data.
You can specify a filter as a query string or in JSON format, just as in the Discover search bar. Click *Add Filter* to
add another filter. Click the image:images/labelbutton.png[Label button icon] *label* button to open the label field, where
you can type in a name to display on the visualization.
*Significant Terms*:: Displays the results of the experimental
{es-ref}search-aggregations-bucket-significantterms-aggregation.html[_significant terms_] aggregation.
Enter a string in the *Custom Label* field to change the display label.
You can click the *Advanced* link to display more customization options for your metrics or bucket aggregation:
*Exclude Pattern*:: Specify a pattern in this field to exclude from the results.
*Include Pattern*:: Specify a pattern in this field to include in the results.
*JSON Input*:: A text field where you can add specific JSON-formatted properties to merge with the aggregation
definition, as in the following example:
[source,shell]
{ "script" : "doc['grade'].value * 1.2" }
The availability of these options varies depending on the aggregation you choose.
Select the *Options* tab to change the following aspects of the chart:
*Show Tooltips*:: Check this box to enable the display of tooltips.
*Highlight*:: Check this box to enable highlighting of elements with same label
*Legend Position*:: You can select where to display the legend (top, left, right, bottom)
*Color Schema*:: You can select an existing color schema or go for custom and define your own colors in the legend
*Reverse Color Schema*:: Checking this checkbox will reverse the color schema.
*Color Scale*:: You can switch between linear, log and sqrt scales for color scale.
*Scale to Data Bounds*:: The default Y axis bounds are zero and the maximum value returned in the data. Check
this box to change both upper and lower bounds to match the values returned in the data.
*Number of Colors*:: Number of color buckets to create. Minimum is 2 and maximum is 10.
*Percentage Mode*:: Enabling this will show legend values as percentages.
*Custom Range*:: You can define custom ranges for your color buckets. For each of the color bucket you need to specify
the minimum value (inclusive) and the maximum value (exclusive) of a range.
*Show Label*:: Enables showing labels with cell values in each cell
*Rotate*:: Allows rotating the cell value label by 90 degrees.
include::visualization-raw-data.asciidoc[]

View file

@ -38,8 +38,7 @@ regularize the display of data sets with variabilities that are themselves highl
the variability is itself variable over the domain being examined, is known as _heteroscedastic_ data. For example, if
a data set of height versus weight has a relatively narrow range of variability at the short end of height, but a wider
range at the taller end, the data set is heteroscedastic.
*Smooth Lines*:: Check this box to curve the line from point to point. Bear in mind that smoothed lines necessarily
affect the representation of your data and create a potential for ambiguity.
*Line Mode*:: You can choose between straight line, smoothed line and stepped line.
*Show Connecting Lines*:: Check this box to draw lines between the points on the chart.
*Show Circles*:: Check this box to draw each data point on the chart as a small circle.
*Current time marker*:: For charts of time-series data, check this box to draw a red line on the current time.

View file

@ -74,6 +74,7 @@
"@bigfunger/jsondiffpatch": "0.1.38-webpack",
"@elastic/datemath": "2.3.0",
"@elastic/kibana-ui-framework": "0.0.13",
"@elastic/webpack-directory-name-as-main": "2.0.2",
"@spalger/filesaver": "1.1.2",
"@spalger/leaflet-draw": "0.2.3",
"@spalger/leaflet-heat": "0.1.3",
@ -166,7 +167,6 @@
"validate-npm-package-name": "2.2.2",
"vision": "4.1.0",
"webpack": "github:elastic/webpack#fix/query-params-for-aliased-loaders",
"webpack-directory-name-as-main": "1.0.0",
"whatwg-fetch": "0.9.0",
"wreck": "6.2.0"
},
@ -217,7 +217,7 @@
"keymirror": "0.1.1",
"license-checker": "5.1.2",
"load-grunt-config": "0.19.2",
"makelogs": "3.1.1",
"makelogs": "3.2.0",
"marked-text-renderer": "0.1.0",
"mocha": "2.5.3",
"murmurhash3js": "3.0.1",

View file

@ -1,4 +0,0 @@
Console
=====
A JSON aware developer's interface to Elasticsearch. Comes with handy machinery such as syntax highlighting, API suggestions, formatting and code folding.

View file

@ -1,7 +1,7 @@
---
root: true
extends: '../../../.eslintrc'
extends: '../../../../.eslintrc'
rules:
block-scoped-var: off

View file

@ -612,7 +612,16 @@ module.exports = function (api) {
min_score: 1.0
},
SCORING_FUNCS
)
),
script: {
__template: {
"script": "_score * doc['f'].value"
},
script: {
//populated by a global rule
}
},
});
};

View file

@ -1,13 +1,14 @@
import Joi from 'joi';
import Boom from 'boom';
import apiServer from './api_server/server';
import { existsSync } from 'fs';
import { resolve, join, sep } from 'path';
import { startsWith, endsWith } from 'lodash';
import { ProxyConfigCollection } from './server/proxy_config_collection';
module.exports = function (kibana) {
let { resolve, join, sep } = require('path');
let Joi = require('joi');
let Boom = require('boom');
let modules = resolve(__dirname, 'public/webpackShims/');
let src = resolve(__dirname, 'public/src/');
let { existsSync } = require('fs');
const { startsWith, endsWith } = require('lodash');
export default function (kibana) {
const modules = resolve(__dirname, 'public/webpackShims/');
const src = resolve(__dirname, 'public/src/');
const apps = [];
@ -88,7 +89,7 @@ module.exports = function (kibana) {
if (!filters.some(re => re.test(uri))) {
const err = Boom.forbidden();
err.output.payload = "Error connecting to '" + uri + "':\n\nUnable to send requests to that url.";
err.output.payload = `Error connecting to '${uri}':\n\nUnable to send requests to that url.`;
err.output.headers['content-type'] = 'text/plain';
reply(err);
} else {
@ -109,19 +110,19 @@ module.exports = function (kibana) {
const filterHeaders = server.plugins.elasticsearch.filterHeaders;
reply.proxy({
mapUri: function (request, done) {
done(null, uri, filterHeaders(request.headers, requestHeadersWhitelist))
done(null, uri, filterHeaders(request.headers, requestHeadersWhitelist));
},
xforward: true,
onResponse(err, res, request, reply, settings, ttl) {
if (err != null) {
reply("Error connecting to '" + uri + "':\n\n" + err.message).type("text/plain").statusCode = 502;
reply(`Error connecting to '${uri}':\n\n${err.message}`).type('text/plain').statusCode = 502;
} else {
reply(null, res);
}
},
...proxyConfigCollection.configForUri(uri)
})
});
}
};
@ -150,14 +151,13 @@ module.exports = function (kibana) {
path: '/api/console/api_server',
method: ['GET', 'POST'],
handler: function (req, reply) {
let server = require('./api_server/server');
let { sense_version, apis } = req.query;
const { sense_version, apis } = req.query;
if (!apis) {
reply(Boom.badRequest('"apis" is a required param.'));
return;
}
return server.resolveApi(sense_version, apis.split(","), reply);
return apiServer.resolveApi(sense_version, apis.split(','), reply);
}
});
@ -190,5 +190,5 @@ module.exports = function (kibana) {
join(src, 'sense_editor/mode/worker.js')
]
}
})
};
});
}

View file

@ -0,0 +1,36 @@
---
root: true
extends: '../../../../.eslintrc'
rules:
block-scoped-var: off
camelcase: off
curly: off
dot-location: off
dot-notation: off
eqeqeq: off
guard-for-in: off
indent: off
max-len: off
new-cap: off
no-caller: off
no-empty: off
no-extend-native: off
no-loop-func: off
no-multi-str: off
no-nested-ternary: off
no-proto: off
no-sequences: off
no-undef: off
no-use-before-define: off
one-var: off
quotes: off
space-before-blocks: off
space-in-parens: off
space-infix-ops: off
semi: off
strict: off
wrap-iife: off
no-var: off
prefer-const: off

View file

@ -18,6 +18,14 @@
border-left: 1px solid #CCC;
}
.ace_multi_string {
color: #166555;
}
.ace_start_triple_quote, .ace_end_triple_quote {
color: #40B0D3;
}
.ace_snippet-marker {
background: rgba(194, 193, 208, 0.20);
border-top: dotted 1px rgba(194, 193, 208, 0.80);

View file

@ -8,7 +8,6 @@ let url_pattern_matcher = require('./autocomplete/url_pattern_matcher');
let _ = require('lodash');
let ext_lang_tools = require('ace/ext-language_tools');
var AceRange = ace.require('ace/range').Range;
var LAST_EVALUATED_TOKEN = null;
@ -226,7 +225,6 @@ module.exports = function (editor) {
return null;
}
if (!context.autoCompleteSet) {
return null; // nothing to do..
}
@ -445,7 +443,6 @@ module.exports = function (editor) {
break; // for now play safe and do nothing. May be made smarter.
}
// go back to see whether we have one of ( : { & [ do not require a comma. All the rest do.
tokenIter = editor.iterForCurrentLoc();
nonEmptyToken = tokenIter.getCurrentToken();
@ -471,7 +468,6 @@ module.exports = function (editor) {
nonEmptyToken = editor.parser.prevNonEmptyToken(tokenIter);
}
switch (nonEmptyToken ? nonEmptyToken.type : "NOTOKEN") {
case "NOTOKEN":
case "paren.lparen":
@ -581,7 +577,6 @@ module.exports = function (editor) {
return context;
}
// needed for scope linking + global term resolving
context.endpointComponentResolver = kb.getEndpointBodyCompleteComponents;
context.globalComponentResolver = kb.getGlobalAutocompleteComponents;
@ -597,7 +592,6 @@ module.exports = function (editor) {
return context;
}
function getCurrentMethodAndTokenPaths(pos) {
var tokenIter = editor.iterForPosition(pos.row, pos.column);
var startPos = pos;
@ -645,7 +639,6 @@ module.exports = function (editor) {
state = STATES.looking_for_scope_start; // skip everything until the beginning of this scope
break;
case "paren.lparen":
bodyTokenPath.unshift(t.value);
if (state == STATES.looking_for_scope_start) {
@ -677,6 +670,29 @@ module.exports = function (editor) {
return {};
}
continue;
case "punctuation.end_triple_quote":
// reset the search for key
state = STATES.looking_for_scope_start;
for (t = tokenIter.stepBackward(); t; t = tokenIter.stepBackward()) {
if (t.type === "punctuation.start_tripple_qoute") {
t = tokenIter.stepBackward();
break;
}
}
if (!t) // oops we run out.. we don't know what's up return null;
{
return {};
}
continue;
case "punctuation.start_triple_quote":
if (state == STATES.start) {
state = STATES.looking_for_key;
}
else if (state == STATES.looking_for_key) {
state = STATES.looking_for_scope_start;
}
bodyTokenPath.unshift('"""');
continue;
case "string":
case "constant.numeric":
case "constant.language.boolean":
@ -761,7 +777,6 @@ module.exports = function (editor) {
t = tokenIter.stepBackward();
}
curUrlPart = null;
while (t && t.type.indexOf("url") != -1) {
switch (t.type) {
@ -814,7 +829,6 @@ module.exports = function (editor) {
return ret;
}
var evaluateCurrentTokenAfterAChange = _.debounce(function evaluateCurrentTokenAfterAChange(pos) {
var session = editor.getSession();
var currentToken = session.getTokenAt(pos.row, pos.column);
@ -843,7 +857,6 @@ module.exports = function (editor) {
return;
}
if (!LAST_EVALUATED_TOKEN) {
LAST_EVALUATED_TOKEN = currentToken;
return; // wait for the next typing.
@ -886,7 +899,6 @@ module.exports = function (editor) {
editor.off("changeSelection", editorChangeListener)
}
function getCompletions(aceEditor, session, pos, prefix, callback) {
try {
@ -949,7 +961,6 @@ module.exports = function (editor) {
}
}
addChangeListener();
// Hook into Ace
@ -997,7 +1008,6 @@ module.exports = function (editor) {
prefix = getAutoCompleteValueFromToken(token);
}
var matches = [];
aceUtils.parForEach(ace_editor.completers, function (completer, next) {
completer.getCompletions(ace_editor, session, pos, prefix, function (err, results) {

View file

@ -16,7 +16,9 @@ let input;
export function initializeInput($el, $actionsEl, $copyAsCurlEl, output) {
input = new SenseEditor($el);
uiModules.get('app/sense').setupResizeCheckerForRootEditors($el, input, output);
// this may not exist if running from tests
let appSense = uiModules.get('app/sense');
appSense.setupResizeCheckerForRootEditors($el, input, output);
input.autocomplete = new Autocomplete(input);
@ -127,7 +129,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output) {
var req = requests.shift();
var es_path = req.url;
var es_method = req.method;
var es_data = req.data.join("\n");
var es_data = utils.collapseLiteralStrings(req.data.join("\n"));
if (es_data) {
es_data += "\n";
} //append a new line for bulk requests.
@ -167,7 +169,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output) {
if (mode === null || mode === "application/json") {
// assume json - auto pretty
try {
value = JSON.stringify(JSON.parse(value), null, 2);
value = utils.expandLiteralStrings(JSON.stringify(JSON.parse(value), null, 2));
}
catch (e) {

View file

@ -136,7 +136,8 @@ function SenseEditor($el) {
var formatted_data = utils.reformatData(parsed_req.data, indent);
if (!formatted_data.changed) {
// toggle.
formatted_data = utils.reformatData(parsed_req.data, !indent);
indent = !indent;
formatted_data = utils.reformatData(parsed_req.data, indent);
}
parsed_req.data = formatted_data.data;
@ -546,8 +547,9 @@ function SenseEditor($el) {
var ret = 'curl -X' + es_method + ' "' + url + '"';
if (es_data && es_data.length) {
ret += " -d'\n";
var data_as_string = utils.collapseLiteralStrings(es_data.join("\n"))
// since Sense doesn't allow single quote json string any single qoute is within a string.
ret += es_data.join("\n").replace(/'/g, '\\"');
ret += data_as_string.replace(/'/g, '\\"');
if (es_data.length > 1) {
ret += "\n";
} // end with a new line

View file

@ -4,6 +4,7 @@ let mode_json = require('ace/mode-json');
var oop = acequire("ace/lib/oop");
var TextMode = acequire("ace/mode/text").Mode;
var ScriptMode = require("./script").ScriptMode;
var MatchingBraceOutdent = acequire("ace/mode/matching_brace_outdent").MatchingBraceOutdent;
var CstyleBehaviour = acequire("ace/mode/behaviour/cstyle").CstyleBehaviour;
var CStyleFoldMode = acequire("ace/mode/folding/cstyle").FoldMode;
@ -20,6 +21,9 @@ var Mode = function () {
this.$outdent = new MatchingBraceOutdent();
this.$behaviour = new CstyleBehaviour();
this.foldingRules = new CStyleFoldMode();
this.createModeDelegates({
"script-": ScriptMode
});
};
oop.inherits(Mode, TextMode);
@ -32,7 +36,7 @@ oop.inherits(Mode, TextMode);
this.getNextLineIndent = function (state, line, tab) {
var indent = this.$getIndent(line);
if (state != "double_q_string") {
if (state !== "string_literal") {
var match = line.match(/^.*[\{\(\[]\s*$/);
if (match) {
indent += tab;

View file

@ -1,4 +1,6 @@
let ace = require('ace');
let x_json = require('./x_json_highlight_rules');
let _ = require('lodash');
var oop = ace.require("ace/lib/oop");
var TextHighlightRules = ace.require("ace/mode/text_highlight_rules").TextHighlightRules;
@ -55,101 +57,18 @@ var InputHighlightRules = function () {
addEOL(["url.param", "url.equal", "url.value"], /([^&=]+)(=)([^&]*)/, "start"),
addEOL(["url.param"], /([^&=]+)/, "start"),
addEOL(["url.amp"], /(&)/, "start")
),
"json": [
{
token: "variable", // single line
regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)'
},
{
token: "string", // single line
regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'
},
{
token: "constant.numeric", // hex
regex: "0[xX][0-9a-fA-F]+\\b"
},
{
token: "constant.numeric", // float
regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
},
{
token: "constant.language.boolean",
regex: "(?:true|false)\\b"
},
{
token: "invalid.illegal", // single quoted strings are not allowed
regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"
},
{
token: "invalid.illegal", // comments are not allowed
regex: "\\/\\/.*$"
},
{
token: "paren.lparen",
merge: false,
regex: "{",
next: "json",
push: true
},
{
token: "paren.lparen",
merge: false,
regex: "[[(]"
},
{
token: "paren.rparen",
merge: false,
regex: "[\\])]"
},
{
token: "paren.rparen",
regex: "}",
merge: false,
next: "pop"
},
{
token: "punctuation.comma",
regex: ","
},
{
token: "punctuation.colon",
regex: ":"
},
{
token: "whitespace",
regex: "\\s+"
},
{
token: "text",
regex: ".+?"
}
],
"double_q_string": [
{
token: "string",
regex: '[^"]+'
},
{
token: "punctuation.end_quote",
regex: '"',
next: "json"
},
{
token: "string",
regex: "",
next: "json"
}
]
)
};
x_json.addToRules(this);
if (this.constructor === InputHighlightRules) {
this.normalizeRules();
}
};
oop.inherits(InputHighlightRules, TextHighlightRules);
module.exports.InputHighlightRules = InputHighlightRules;

View file

@ -1,14 +1,15 @@
let ace = require('ace');
let ace_mode_json = require('ace/mode-json');
let x_json = require('./x_json_highlight_rules');
var oop = ace.require("ace/lib/oop");
var JsonHighlightRules = ace.require("ace/mode/json_highlight_rules").JsonHighlightRules;
var OutputJsonHighlightRules = function () {
// regexp must not have capturing parentheses. Use (?:) instead.
// regexps are ordered -> the first match is used
this.$rules = new JsonHighlightRules().getRules();
this.$rules = {};
x_json.addToRules(this, 'start');
this.$rules.start.unshift(
{
@ -17,6 +18,10 @@ var OutputJsonHighlightRules = function () {
}
);
if (this.constructor === OutputJsonHighlightRules) {
this.normalizeRules();
}
};
oop.inherits(OutputJsonHighlightRules, JsonHighlightRules);

View file

@ -0,0 +1,61 @@
let ace = require('ace');
let acequire = require('acequire');
let mode_json = require('ace/mode-json');
var oop = acequire("ace/lib/oop");
var TextMode = acequire("ace/mode/text").Mode;
var MatchingBraceOutdent = acequire("ace/mode/matching_brace_outdent").MatchingBraceOutdent;
var CstyleBehaviour = acequire("ace/mode/behaviour/cstyle").CstyleBehaviour;
var CStyleFoldMode = acequire("ace/mode/folding/cstyle").FoldMode;
var AceTokenizer = acequire("ace/tokenizer").Tokenizer;
var ScriptHighlightRules = require("./script_highlight_rules").ScriptHighlightRules;
export var ScriptMode = function () {
this.$outdent = new MatchingBraceOutdent();
this.$behaviour = new CstyleBehaviour();
this.foldingRules = new CStyleFoldMode();
};
oop.inherits(ScriptMode, TextMode);
(function () {
this.HighlightRules = ScriptHighlightRules;
this.getNextLineIndent = function (state, line, tab) {
var indent = this.$getIndent(line);
var match = line.match(/^.*[\{\[]\s*$/);
if (match) {
indent += tab;
}
return indent;
};
this.checkOutdent = function (state, line, input) {
return this.$outdent.checkOutdent(line, input);
};
this.autoOutdent = function (state, doc, row) {
this.$outdent.autoOutdent(doc, row);
};
// this.createWorker = function (session) {
// var worker = new WorkerClient(["ace", "sense_editor"], "sense_editor/mode/worker", "SenseWorker");
// worker.attachToDocument(session.getDocument());
// worker.on("error", function (e) {
// session.setAnnotations([e.data]);
// });
// worker.on("ok", function (anno) {
// session.setAnnotations(anno.data);
// });
// return worker;
// };
}).call(ScriptMode.prototype);

View file

@ -0,0 +1,65 @@
let ace = require('ace');
let oop = ace.require("ace/lib/oop");
let TextHighlightRules = ace.require("ace/mode/text_highlight_rules").TextHighlightRules;
let painlessKeywords = (
"def|int|long|byte|String|float|double|char|null|if|else|while|do|for|continue|break|new|try|catch|throw|this|instanceof|return|ctx"
);
export var ScriptHighlightRules = function () {
this.name = "ScriptHighlightRules";
this.$rules = {
"start": [
{
token: "script.comment",
regex: "\\/\\/.*$"
},
{
token : "script.string.regexp",
regex : "[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"
},
{
token : "script.string", // single line
regex : "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"
},
{
token : "script.constant.numeric", // hex
regex : "0[xX][0-9a-fA-F]+\\b"
},
{
token : "script.constant.numeric", // float
regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
},
{
token : "script.constant.language.boolean",
regex : "(?:true|false)\\b"
},
{
token: "script.keyword",
regex: painlessKeywords
},
{
token : "script.text",
regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
},
{
token : "script.keyword.operator",
regex : "\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)"
},
{
token : "script.lparen",
regex : "[[({]"
},
{
token : "script.rparen",
regex : "[\\])}]"
},
{
token : "script.text",
regex : "\\s+"
}
]
}
};
oop.inherits(ScriptHighlightRules, TextHighlightRules);

View file

@ -1367,6 +1367,20 @@ define("sense_editor/mode/worker_parser", ['require', 'exports', 'module' ], fun
return ch;
},
nextUpTo = function (upTo, errorMessage) {
var currentAt = at,
i = text.indexOf(upTo, currentAt);
if (i < 0) {
error(errorMessage || "Expected '" + upTo + "'");
}
reset(i + upTo.length);
return text.substring(currentAt, i);
},
peek = function (c) {
return text.substr(at, c.length) === c; // nocommit - double check
},
number = function () {
var number,
@ -1414,29 +1428,36 @@ define("sense_editor/mode/worker_parser", ['require', 'exports', 'module' ], fun
uffff;
if (ch === '"') {
while (next()) {
if (ch === '"') {
next();
return string;
} else if (ch === '\\') {
next();
if (ch === 'u') {
uffff = 0;
for (i = 0; i < 4; i += 1) {
hex = parseInt(next(), 16);
if (!isFinite(hex)) {
break;
if (peek('""')) {
// literal
next('"');
next('"');
return nextUpTo('"""', "failed to find closing '\"\"\"'");
} else {
while (next()) {
if (ch === '"') {
next();
return string;
} else if (ch === '\\') {
next();
if (ch === 'u') {
uffff = 0;
for (i = 0; i < 4; i += 1) {
hex = parseInt(next(), 16);
if (!isFinite(hex)) {
break;
}
uffff = uffff * 16 + hex;
}
uffff = uffff * 16 + hex;
string += String.fromCharCode(uffff);
} else if (typeof escapee[ch] === 'string') {
string += escapee[ch];
} else {
break;
}
string += String.fromCharCode(uffff);
} else if (typeof escapee[ch] === 'string') {
string += escapee[ch];
} else {
break;
string += ch;
}
} else {
string += ch;
}
}
}

View file

@ -0,0 +1,111 @@
let _ = require("lodash");
let ScriptHighlightRules = require("./script_highlight_rules").ScriptHighlightRules;
var jsonRules = function (root) {
root = root ? root : "json";
var rules = {};
rules[root] = [
{
token: ["variable", "whitespace", "ace.punctuation.colon", "whitespace", "punctuation.start_triple_quote"],
regex: '("script"|"inline")(\\s*?)(:)(\\s*?)(""")',
next: "script-start",
merge: false,
push: true
},
{
token: "variable", // single line
regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]\\s*(?=:)'
},
{
token: "punctuation.start_triple_quote",
regex: '"""',
next: "string_literal",
merge: false,
push: true
},
{
token: "string", // single line
regex: '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'
},
{
token: "constant.numeric", // hex
regex: "0[xX][0-9a-fA-F]+\\b"
},
{
token: "constant.numeric", // float
regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
},
{
token: "constant.language.boolean",
regex: "(?:true|false)\\b"
},
{
token: "invalid.illegal", // single quoted strings are not allowed
regex: "['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"
},
{
token: "invalid.illegal", // comments are not allowed
regex: "\\/\\/.*$"
},
{
token: "paren.lparen",
merge: false,
regex: "{",
next: root,
push: true
},
{
token: "paren.lparen",
merge: false,
regex: "[[(]"
},
{
token: "paren.rparen",
merge: false,
regex: "[\\])]"
},
{
token: "paren.rparen",
regex: "}",
merge: false,
next: "pop"
},
{
token: "punctuation.comma",
regex: ","
},
{
token: "punctuation.colon",
regex: ":"
},
{
token: "whitespace",
regex: "\\s+"
},
{
token: "text",
regex: ".+?"
}
];
rules["string_literal"] = [
{
token: "punctuation.end_triple_quote",
regex: '"""',
next: "pop"
},
{
token: "multi_string",
regex: "."
}
];
return rules;
};
module.exports.addToRules = function (otherRules, embedUnder) {
otherRules.$rules = _.defaultsDeep(otherRules.$rules, jsonRules(embedUnder));
otherRules.embedRules(ScriptHighlightRules, "script-", [{
token: "punctuation.end_triple_quote",
regex: '"""',
next : "pop",
}]);
}

View file

@ -18,7 +18,10 @@ utils.reformatData = function (data, indent) {
for (var i = 0; i < data.length; i++) {
var cur_doc = data[i];
try {
var new_doc = utils.jsonToString(JSON.parse(cur_doc), indent ? 2 : 0);
var new_doc = utils.jsonToString(JSON.parse(utils.collapseLiteralStrings(cur_doc)), indent ? 2 : 0);
if (indent) {
new_doc = utils.expandLiteralStrings(new_doc);
}
changed = changed || new_doc != cur_doc;
formatted_data.push(new_doc);
}
@ -34,5 +37,23 @@ utils.reformatData = function (data, indent) {
};
};
utils.collapseLiteralStrings = function (data) {
return data.replace(/"""(?:\s*\n)?((?:.|\n)*?)(?:\n\s*)?"""/g,function (match, literal) {
return JSON.stringify(literal);
});
}
utils.expandLiteralStrings = function (data) {
return data.replace(/("(?:\\"|[^"])*?")/g, function (match, string) {
// expand things with two slashes or more
if (string.split(/\\./).length > 2) {
string = JSON.parse(string).replace("^\s*\n", "").replace("\n\s*^", "");
var append = string.includes("\n") ? "\n" : ""; // only go multi line if the string has multiline
return '"""' + append + string + append + '"""';
} else {
return string;
}
});
}
module.exports = utils;

View file

@ -1,17 +1,19 @@
let ace = require('ace');
let es = require('../../src/es');
let input = require('../../src/input');
import { initializeInput } from '../../src/input';
let editor_input1 = require('raw!./editor_input1.txt');
let utils = require('../../src/utils');
var aceRange = ace.require("ace/range");
var { test, module, ok, fail, asyncTest, deepEqual, equal, start } = QUnit;
let input;
module("Editor", {
setup: function () {
input = initializeInput($('#editor'), $('#editor_actions'), $('#copy_as_curl'), null);
input.$el.show();
input.autocomplete._test.removeChangeListener();
},
teardown: function () {
input.$el.hide();
@ -86,7 +88,6 @@ var multi_doc_request = {
};
multi_doc_request.data = multi_doc_request.data_as_array.join("\n");
utils_test("simple request range", simple_request.prefix, simple_request.data, function () {
input.getRequestRange(function (range) {
var expected = new aceRange.Range(
@ -257,6 +258,42 @@ utils_test("multi doc request data", multi_doc_request.prefix, multi_doc_request
});
});
var script_request = {
prefix: 'POST _search',
data: [
'{',
' "query": { "script": """',
' some script ',
' """}',
'}'
].join('\n')
};
utils_test("script request range", script_request.prefix, script_request.data, function () {
input.getRequestRange(function (range) {
var expected = new aceRange.Range(
0, 0,
5, 1
);
compareRequest(range, expected);
start();
});
});
utils_test("simple request data", simple_request.prefix, simple_request.data, function () {
input.getRequest(function (request) {
var expected = {
method: "POST",
url: "_search",
data: [utils.collapseLiteralStrings(simple_request.data)]
};
compareRequest(request, expected);
start();
});
});
function multi_req_test(name, editor_input, range, expected) {
utils_test("multi request select - " + name, editor_input, function () {
input.getRequestsInRange(range, function (requests) {

View file

@ -1,4 +1,5 @@
let input = require('../../src/input');
import { initializeInput } from '../../src/input';
let input;
let kb = require('../../src/kb');
let mappings = require('../../src/mappings');
let $ = require('jquery');
@ -7,11 +8,12 @@ var { test, module, ok, fail, asyncTest, deepEqual, equal, start } = QUnit;
module("Integration", {
setup: function () {
$("#editor_container").show();
input = initializeInput($('#editor'), $('#editor_actions'), $('#copy_as_curl'), null);
input.$el.show();
input.autocomplete._test.removeChangeListener();
},
teardown: function () {
$("#editor_container").hide();
input.$el.hide();
input.autocomplete._test.addChangeListener();
}
});

View file

@ -1,12 +1,15 @@
let ace = require('ace');
let $ = require('jquery');
let input = require('../../src/input');
import { initializeInput } from '../../src/input';
let input;
var token_iterator = ace.require("ace/token_iterator");
var { test, module, ok, fail, asyncTest, deepEqual, equal, start } = QUnit;
module("Tokenization", {
setup: function () {
input = initializeInput($('#editor'), $('#editor_actions'), $('#copy_as_curl'), null);
input.$el.show();
input.autocomplete._test.removeChangeListener();
},
@ -282,3 +285,93 @@ states_test(
' }\n' +
'}'
);
states_test(
["start", "json", "json", "start"],
'POST _search\n' +
'{\n' +
' "script": { "inline": "" }\n' +
'}'
);
states_test(
["start", "json", "json", "start"],
'POST _search\n' +
'{\n' +
' "script": ""\n' +
'}'
);
states_test(
["start", "json", ["json", "json"], "json", "start"],
'POST _search\n' +
'{\n' +
' "script": {\n' +
' }\n' +
'}'
);
states_test(
["start", "json", ["script-start", "json", "json", "json"], ["script-start", "json", "json", "json"],
["json", "json"], "json", "start"],
'POST _search\n' +
'{\n' +
' "test": { "script": """\n' +
' test script\n' +
' """\n' +
' }\n' +
'}'
);
states_test(
["start", "json", ["script-start", "json"], ["script-start", "json"], "json", "start"],
'POST _search\n' +
'{\n' +
' "script": """\n' +
' test script\n' +
' """,\n' +
'}'
);
states_test(
["start", "json", "json", "start"],
'POST _search\n' +
'{\n' +
' "script": """test script""",\n' +
'}'
);
states_test(
["start", "json", ["string_literal", "json"], ["string_literal", "json"], "json", "start"],
'POST _search\n' +
'{\n' +
' "somthing": """\n' +
' test script\n' +
' """,\n' +
'}'
);
states_test(
["start", "json", ["string_literal", "json", "json", "json"], ["string_literal", "json", "json", "json"],
["json", "json"], ["json", "json"],
"json", "start"],
'POST _search\n' +
'{\n' +
' "somthing": { "f" : """\n' +
' test script\n' +
' """,\n' +
' "g": 1\n' +
' }\n' +
'}'
);
states_test(
["start", "json", "json", "start"],
'POST _search\n' +
'{\n' +
' "something": """test script""",\n' +
'}'
);

View file

@ -0,0 +1,34 @@
==========
String only 1
-------------------------------------
""" hello
to you """
-------------------------------------
" hello\nto you "
==========
String only 2
-------------------------------------
"""
startning with new lines and ending as well
"""
-------------------------------------
"startning with new lines and ending as well"
==========
Strings in requests
-------------------------------------
{
"f": { "somefield" : """
test
test2
""" },
"g": { "script" : """second + "\";""" },
"h": 1,
"script": "a + 2"
}
-------------------------------------
{
"f": { "somefield" : "test\ntest2" },
"g": { "script" : "second + \"\\\";" },
"h": 1,
"script": "a + 2"
}

View file

@ -0,0 +1,21 @@
==========
Scripts in requests
-------------------------------------
{
"f": { "script" : { "inline": "test\ntest\\2" } },
"g": { "script" : "second + \"\\\";" },
"f": "short with \\",
"h": 1,
"script": "a + 2"
}
-------------------------------------
{
"f": { "script" : { "inline": """
test
test\2
""" } },
"g": { "script" : """second + "\";""" },
"f": "short with \\",
"h": 1,
"script": "a + 2"
}

View file

@ -0,0 +1,36 @@
let _ = require('lodash');
let utils = require('../../src/utils');
let collapsingTests = require('raw!./utils_string_collapsing.txt');
let expandingTests = require('raw!./utils_string_expanding.txt');
var { test, module, ok, fail, asyncTest, deepEqual, equal, start } = QUnit;
module("Utils class");
_.each(collapsingTests.split(/^=+$/m), function (fixture) {
if (fixture.trim() == "") {
return;
}
fixture = fixture.split(/^-+$/m);
var name = fixture[0].trim(),
expanded = fixture[1].trim(),
collapsed = fixture[2].trim();
test("Literal collapse - " + name, function () {
deepEqual(utils.collapseLiteralStrings(expanded), collapsed);
});
});
_.each(expandingTests.split(/^=+$/m), function (fixture) {
if (fixture.trim() == "") {
return;
}
fixture = fixture.split(/^-+$/m);
var name = fixture[0].trim(),
collapsed = fixture[1].trim(),
expanded = fixture[2].trim();
test("Literal expand - " + name, function () {
deepEqual(utils.expandLiteralStrings(collapsed), expanded);
});
});

View file

@ -1,5 +1,14 @@
require('ace');
const module = require('ui/modules').get('app/sense');
module.run(function (Private, $rootScope) {
module.setupResizeCheckerForRootEditors = ($el, ...editors) => {
// mock the resize checker
};
});
require('ui/chrome')
.setRootTemplate(require('./index.html'))
.setRootController(function () {
@ -11,6 +20,7 @@ require('ui/chrome')
QUnit.config.autostart = false;
QUnit.init();
require('./src/utils_tests.js');
require('./src/url_autocomplete_tests.js');
require('./src/url_params_tests.js');
require('./src/curl_parsing_tests.js');

View file

@ -6,20 +6,20 @@ import fs from 'fs';
import https, { Agent as HttpsAgent } from 'https';
import { parse as parseUrl } from 'url';
import { ProxyConfig } from '../proxy_config'
import { ProxyConfig } from '../proxy_config';
const matchGoogle = {
protocol: 'https',
host: 'google.com',
path: '/search'
}
};
const parsedGoogle = parseUrl('https://google.com/search');
const parsedLocalEs = parseUrl('https://localhost:5601/search');
describe('ProxyConfig', function () {
beforeEach(function () {
sinon.stub(fs, 'readFileSync', function (path) {
return { path }
return { path };
});
});

View file

@ -5,7 +5,7 @@ import sinon from 'sinon';
import fs from 'fs';
import { Agent as HttpsAgent } from 'https';
import { ProxyConfigCollection } from '../proxy_config_collection'
import { ProxyConfigCollection } from '../proxy_config_collection';
describe('ProxyConfigCollection', function () {
beforeEach(function () {
@ -60,7 +60,7 @@ describe('ProxyConfigCollection', function () {
timeout: 5
}
]
];
function getTimeout(uri) {
const collection = new ProxyConfigCollection(proxyConfigs);
@ -69,7 +69,7 @@ describe('ProxyConfigCollection', function () {
context('http://localhost:5601', function () {
it('defaults to the first matching timeout', function () {
expect(getTimeout('http://localhost:5601')).to.be(3)
expect(getTimeout('http://localhost:5601')).to.be(3);
});
});

View file

@ -1,7 +1,7 @@
/* eslint-env mocha */
import expect from 'expect.js'
import expect from 'expect.js';
import { WildcardMatcher } from '../wildcard_matcher'
import { WildcardMatcher } from '../wildcard_matcher';
function should(candidate, ...constructorArgs) {
if (!new WildcardMatcher(...constructorArgs).match(candidate)) {

View file

@ -1,14 +1,14 @@
import { memoize, values } from 'lodash'
import { format as formatUrl } from 'url'
import { Agent as HttpsAgent } from 'https'
import { readFileSync } from 'fs'
import { memoize, values } from 'lodash';
import { format as formatUrl } from 'url';
import { Agent as HttpsAgent } from 'https';
import { readFileSync } from 'fs';
import { WildcardMatcher } from './wildcard_matcher'
import { WildcardMatcher } from './wildcard_matcher';
const makeHttpsAgent = memoize(
opts => new HttpsAgent(opts),
opts => JSON.stringify(opts)
)
);
export class ProxyConfig {
constructor(config) {

View file

@ -1,12 +1,12 @@
import { defaultsDeep } from 'lodash'
import { defaultsDeep } from 'lodash';
import { ProxyConfig } from './proxy_config'
import { parse as parseUrl } from 'url'
import { ProxyConfig } from './proxy_config';
import { parse as parseUrl } from 'url';
export class ProxyConfigCollection {
constructor(configs = []) {
this.configs = configs.map(settings => new ProxyConfig(settings))
this.configs = configs.map(settings => new ProxyConfig(settings));
}
configForUri(uri) {

View file

@ -1,4 +1,4 @@
import { Minimatch } from 'minimatch'
import { Minimatch } from 'minimatch';
export class WildcardMatcher {
constructor(wildcardPattern, emptyVal) {
@ -10,7 +10,7 @@ export class WildcardMatcher {
nocase: true,
matchBase: true,
nocomment: true
})
});
}
match(candidate) {
@ -19,6 +19,6 @@ export class WildcardMatcher {
return true;
}
return this.matcher.match(candidate || '')
return this.matcher.match(candidate || '');
}
}

View file

@ -1,8 +1,13 @@
import { trim, trimRight } from 'lodash';
import { trim, trimRight, bindKey, get } from 'lodash';
import { methodNotAllowed } from 'boom';
import healthCheck from './lib/health_check';
import exposeClient from './lib/expose_client';
import { createDataCluster } from './lib/create_data_cluster';
import { createAdminCluster } from './lib/create_admin_cluster';
import { clientLogger } from './lib/client_logger';
import { createClusters } from './lib/create_clusters';
import filterHeaders from './lib/filter_headers';
import createProxy, { createPath } from './lib/create_proxy';
const DEFAULT_REQUEST_HEADERS = [ 'authorization' ];
@ -26,6 +31,7 @@ module.exports = function ({ Plugin }) {
customHeaders: object().default({}),
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
ssl: object({
verify: boolean().default(true),
ca: array().single().items(string()),
@ -33,6 +39,29 @@ module.exports = function ({ Plugin }) {
key: string()
}).default(),
apiVersion: Joi.string().default('master'),
healthCheck: object({
delay: number().default(2500)
}).default(),
tribe: object({
url: string().uri({ scheme: ['http', 'https'] }),
preserveHost: boolean().default(true),
username: string(),
password: string(),
shardTimeout: number().default(0),
requestTimeout: number().default(30000),
requestHeadersWhitelist: array().items().single().default(DEFAULT_REQUEST_HEADERS),
customHeaders: object().default({}),
pingTimeout: number().default(ref('requestTimeout')),
startupTimeout: number().default(5000),
logQueries: boolean().default(false),
ssl: object({
verify: boolean().default(true),
ca: array().single().items(string()),
cert: string(),
key: string()
}).default(),
apiVersion: Joi.string().default('master'),
}).default()
}).default();
},
@ -42,15 +71,24 @@ module.exports = function ({ Plugin }) {
esRequestTimeout: options.requestTimeout,
esShardTimeout: options.shardTimeout,
esApiVersion: options.apiVersion,
esDataIsTribe: get(options, 'tribe.url') ? true : false,
};
}
},
init(server, options) {
const kibanaIndex = server.config().get('kibana.index');
const clusters = createClusters(server);
server.expose('getCluster', clusters.get);
server.expose('createCluster', clusters.create);
server.expose('filterHeaders', filterHeaders);
server.expose('ElasticsearchClientLogging', clientLogger(server));
createDataCluster(server);
createAdminCluster(server);
// Expose the client to the server
exposeClient(server);
createProxy(server, 'GET', '/{paths*}');
createProxy(server, 'POST', '/_mget');
createProxy(server, 'POST', '/{index}/_search');
@ -69,7 +107,7 @@ module.exports = function ({ Plugin }) {
function noDirectIndex({ path }, reply) {
const requestPath = trimRight(trim(path), '/');
const matchPath = createPath(kibanaIndex);
const matchPath = createPath('/elasticsearch', kibanaIndex);
if (requestPath === matchPath) {
return reply(methodNotAllowed('You cannot modify the primary kibana index through this interface.'));

View file

@ -0,0 +1,134 @@
import expect from 'expect.js';
import { Cluster } from '../cluster';
import sinon from 'sinon';
import { errors as esErrors } from 'elasticsearch';
import { set, partial, cloneDeep } from 'lodash';
import Boom from 'boom';
describe('plugins/elasticsearch', function () {
describe('cluster', function () {
let cluster;
const config = {
url: 'http://localhost:9200',
ssl: { verify: false },
requestHeadersWhitelist: [ 'authorization' ]
};
beforeEach(() => {
cluster = new Cluster(config);
});
it('persists the config', () => {
expect(cluster._config).to.eql(config);
});
it('exposes error definitions', () => {
expect(cluster.errors).to.be(esErrors);
});
it('closes the clients', () => {
cluster._client.close = sinon.spy();
cluster._noAuthClient.close = sinon.spy();
cluster.close();
sinon.assert.calledOnce(cluster._client.close);
sinon.assert.calledOnce(cluster._noAuthClient.close);
});
it('protects the config from changes', () => {
const localRequestHeadersWhitelist = cluster.getRequestHeadersWhitelist();
expect(localRequestHeadersWhitelist.length).to.not.equal(config.requestHeadersWhitelist);
});
describe('callWithInternalUser', () => {
let client;
beforeEach(() => {
client = cluster._client = sinon.stub();
set(client, 'nodes.info', sinon.stub().returns(Promise.resolve()));
});
it('should return a function', () => {
expect(cluster.callWithInternalUser).to.be.a('function');
});
it('throws an error for an invalid endpoint', () => {
const fn = partial(cluster.callWithInternalUser, 'foo');
expect(fn).to.throwException(/called with an invalid endpoint: foo/);
});
it('calls the client with params', () => {
const params = { foo: 'Foo' };
cluster.callWithInternalUser('nodes.info', params);
sinon.assert.calledOnce(client.nodes.info);
expect(client.nodes.info.getCall(0).args[0]).to.eql(params);
});
});
describe('callWithRequest', () => {
let client;
beforeEach(() => {
client = cluster._noAuthClient = sinon.stub();
set(client, 'nodes.info', sinon.stub().returns(Promise.resolve()));
});
it('should return a function', () => {
expect(cluster.callWithRequest).to.be.a('function');
});
it('throws an error for an invalid endpoint', () => {
const fn = partial(cluster.callWithRequest, {}, 'foo');
expect(fn).to.throwException(/called with an invalid endpoint: foo/);
});
it('calls the client with params', () => {
const params = { foo: 'Foo' };
cluster.callWithRequest({}, 'nodes.info', params);
sinon.assert.calledOnce(client.nodes.info);
expect(client.nodes.info.getCall(0).args[0]).to.eql(params);
});
it('passes only whitelisted headers', () => {
const headers = { authorization: 'Basic TEST' };
const request = { headers: Object.assign({}, headers, { foo: 'Foo' }) };
cluster.callWithRequest(request, 'nodes.info');
sinon.assert.calledOnce(client.nodes.info);
expect(client.nodes.info.getCall(0).args[0]).to.eql({
headers: headers
});
});
describe('wrap401Errors', () => {
let handler;
const error = new Error('Authentication required');
error.statusCode = 401;
beforeEach(() => {
handler = sinon.stub();
});
it('ensures WWW-Authenticate header', async () => {
set(client, 'mock.401', sinon.stub().returns(Promise.reject(error)));
await cluster.callWithRequest({}, 'mock.401', {}, { wrap401Errors: true }).catch(handler);
sinon.assert.calledOnce(handler);
expect(handler.getCall(0).args[0].output.headers['WWW-Authenticate']).to.eql('Basic realm="Authorization Required"');
});
it('persists WWW-Authenticate header', async () => {
set(error, 'body.error.header[WWW-Authenticate]', 'Basic realm="Test"');
set(client, 'mock.401', sinon.stub().returns(Promise.reject(error)));
await cluster.callWithRequest({}, 'mock.401', {}, { wrap401Errors: true }).catch(handler);
sinon.assert.calledOnce(handler);
expect(handler.getCall(0).args[0].output.headers['WWW-Authenticate']).to.eql('Basic realm="Test"');
});
});
});
});
});

View file

@ -0,0 +1,66 @@
import expect from 'expect.js';
import sinon from 'sinon';
import { bindKey, set, get, partial } from 'lodash';
import { createAdminCluster } from '../create_admin_cluster';
describe('plugins/elasticsearch', function () {
describe('create_admin_cluster', function () {
let cluster;
let server;
beforeEach(() => {
const config = {
elasticsearch: {
url: 'http://localhost:9200',
logQueries: true
}
};
server = sinon.spy();
cluster = {
close: sinon.spy()
};
set(server, 'plugins.elasticsearch.createCluster', sinon.mock().returns(cluster));
set(server, 'on', sinon.spy());
server.config = () => {
return { get: partial(get, config) };
};
createAdminCluster(server);
});
it('creates the cluster', () => {
const { createCluster } = server.plugins.elasticsearch;
sinon.assert.calledOnce(createCluster);
expect(createCluster.getCall(0).args[0]).to.eql('admin');
expect(createCluster.getCall(0).args[1].url).to.eql('http://localhost:9200');
});
it('sets client logger for cluster options', () => {
const { createCluster } = server.plugins.elasticsearch;
const firstCall = createCluster.getCall(0);
const Log = firstCall.args[1].log;
const logger = new Log;
sinon.assert.calledOnce(createCluster);
expect(firstCall.args[0]).to.eql('admin');
expect(firstCall.args[1].url).to.eql('http://localhost:9200');
expect(logger.tags).to.eql(['admin']);
expect(logger.logQueries).to.eql(true);
});
it('close cluster of server close', () => {
const clusterClose = server.on.getCall(0).args[1];
clusterClose();
sinon.assert.calledOnce(cluster.close);
sinon.assert.calledOnce(server.on);
expect(server.on.getCall(0).args[0]).to.eql('close');
});
});
});

View file

@ -0,0 +1,50 @@
import expect from 'expect.js';
import { createClusters } from '../create_clusters';
import { Cluster } from '../cluster';
import sinon from 'sinon';
import { partial } from 'lodash';
describe('plugins/elasticsearch', function () {
describe('createClusters', function () {
let clusters;
let server;
beforeEach(() => {
server = {
plugins: {
elasticsearch: {}
},
expose: sinon.mock()
};
clusters = createClusters(server);
});
describe('createCluster', () => {
let cluster;
const config = {
url: 'http://localhost:9200',
ssl: {
verify: false
}
};
beforeEach(() => {
cluster = clusters.create('admin', config);
});
it('returns a cluster', () => {
expect(cluster).to.be.a(Cluster);
});
it('persists the cluster', () => {
expect(clusters.get('admin')).to.be.a(Cluster);
});
it('throws if cluster already exists', () => {
const fn = partial(clusters.create, 'admin', config);
expect(fn).to.throwException(/cluster \'admin\' already exists/);
});
});
});
});

View file

@ -0,0 +1,85 @@
import expect from 'expect.js';
import sinon from 'sinon';
import { bindKey, set, get, partial } from 'lodash';
import { createDataCluster } from '../create_data_cluster';
describe('plugins/elasticsearch', function () {
describe('create_data_cluster', function () {
let cluster;
let server;
let config;
beforeEach(() => {
config = {
elasticsearch: {
url: 'http://localhost:9200',
logQueries: true
}
};
server = sinon.spy();
cluster = {
close: sinon.spy()
};
set(server, 'plugins.elasticsearch.createCluster', sinon.mock().returns(cluster));
set(server, 'on', sinon.spy());
server.config = () => {
return { get: partial(get, config) };
};
});
it('creates the cluster with elasticsearch config', () => {
createDataCluster(server);
const { createCluster } = server.plugins.elasticsearch;
sinon.assert.calledOnce(createCluster);
expect(createCluster.getCall(0).args[0]).to.eql('data');
expect(createCluster.getCall(0).args[1].url).to.eql('http://localhost:9200');
});
it('creates the cluster with elasticsearch.tribe config', () => {
config.elasticsearch.tribe = {
url: 'http://localhost:9201'
};
createDataCluster(server);
const { createCluster } = server.plugins.elasticsearch;
sinon.assert.calledOnce(createCluster);
expect(createCluster.getCall(0).args[0]).to.eql('data');
expect(createCluster.getCall(0).args[1].url).to.eql('http://localhost:9201');
});
it('sets client logger for cluster options', () => {
createDataCluster(server);
const { createCluster } = server.plugins.elasticsearch;
const firstCall = createCluster.getCall(0);
const Log = firstCall.args[1].log;
const logger = new Log;
sinon.assert.calledOnce(createCluster);
expect(firstCall.args[0]).to.eql('data');
expect(firstCall.args[1].url).to.eql('http://localhost:9200');
expect(logger.tags).to.eql(['data']);
expect(logger.logQueries).to.eql(true);
});
it('close cluster of server close', () => {
createDataCluster(server);
const clusterClose = server.on.getCall(0).args[1];
clusterClose();
sinon.assert.calledOnce(cluster.close);
sinon.assert.calledOnce(server.on);
expect(server.on.getCall(0).args[0]).to.eql('close');
});
});
});

View file

@ -9,39 +9,45 @@ describe('plugins/elasticsearch', function () {
describe('lib/create_kibana_index', function () {
let server;
let client;
let callWithInternalUser;
let cluster;
beforeEach(function () {
server = {};
client = {};
let config = { kibana: { index: '.my-kibana' } };
const get = sinon.stub();
get.returns(config);
get.withArgs('kibana.index').returns(config.kibana.index);
config = function () { return { get: get }; };
_.set(client, 'indices.create', sinon.stub());
_.set(client, 'cluster.health', sinon.stub());
_.set(server, 'plugins.elasticsearch.client', client);
_.set(server, 'plugins.elasticsearch', {});
_.set(server, 'config', config);
callWithInternalUser = sinon.stub();
cluster = { callWithInternalUser: callWithInternalUser };
server.plugins.elasticsearch.getCluster = sinon.stub().withArgs('admin').returns(cluster);
});
describe('successful requests', function () {
beforeEach(function () {
client.indices.create.returns(Promise.resolve());
client.cluster.health.returns(Promise.resolve());
callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve());
callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(Promise.resolve());
});
it('should check cluster.health upon successful index creation', function () {
const fn = createKibanaIndex(server);
return fn.then(function () {
sinon.assert.calledOnce(client.cluster.health);
sinon.assert.calledOnce(callWithInternalUser.withArgs('cluster.health', sinon.match.any));
});
});
it('should be created with mappings for config.buildNum', function () {
const fn = createKibanaIndex(server);
return fn.then(function () {
const params = client.indices.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params)
.to.have.property('body');
expect(params.body)
@ -60,7 +66,7 @@ describe('plugins/elasticsearch', function () {
it('should be created with 1 shard and default replica', function () {
const fn = createKibanaIndex(server);
return fn.then(function () {
const params = client.indices.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params)
.to.have.property('body');
expect(params.body)
@ -75,19 +81,17 @@ describe('plugins/elasticsearch', function () {
it('should be created with index name set in the config', function () {
const fn = createKibanaIndex(server);
return fn.then(function () {
const params = client.indices.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params)
.to.have.property('index', '.my-kibana');
});
});
});
describe('failure requests', function () {
it('should reject with an Error', function () {
const error = new Error('Oops!');
client.indices.create.returns(Promise.reject(error));
callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.reject(error));
const fn = createKibanaIndex(server);
return fn.catch(function (err) {
expect(err).to.be.a(Error);
@ -96,24 +100,21 @@ describe('plugins/elasticsearch', function () {
it('should reject with an error if index creation fails', function () {
const error = new Error('Oops!');
client.indices.create.returns(Promise.reject(error));
callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.reject(error));
const fn = createKibanaIndex(server);
return fn.catch(function (err) {
expect(err.message).to.be('Unable to create Kibana index ".my-kibana"');
});
});
it('should reject with an error if health check fails', function () {
const error = new Error('Oops!');
client.indices.create.returns(Promise.resolve());
client.cluster.health.returns(Promise.reject(error));
callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve());
callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(Promise.reject(new Error()));
const fn = createKibanaIndex(server);
return fn.catch(function (err) {
expect(err.message).to.be('Waiting for Kibana index ".my-kibana" to come online failed.');
});
});
});
});
});

View file

@ -3,20 +3,21 @@ import createProxy from '../create_proxy';
describe('plugins/elasticsearch', function () {
describe('lib/create_proxy', function () {
describe('#createPath', function () {
it('prepends /elasticsearch to route', function () {
const path = createProxy.createPath('/wat');
expect(path).to.equal('/elasticsearch/wat');
const path = createProxy.createPath('/foobar', '/wat');
expect(path).to.equal('/foobar/wat');
});
context('when arg does not start with a slash', function () {
it('adds slash anyway', function () {
const path = createProxy.createPath('wat');
expect(path).to.equal('/elasticsearch/wat');
});
it('ensures leading slash for prefix', function () {
const path = createProxy.createPath('foobar', '/wat');
expect(path).to.equal('/foobar/wat');
});
it('ensures leading slash for path', function () {
const path = createProxy.createPath('/foobar', 'wat');
expect(path).to.equal('/foobar/wat');
});
});
});
});

View file

@ -5,23 +5,22 @@ import expect from 'expect.js';
import url from 'url';
import serverConfig from '../../../../../test/server_config';
import checkEsVersion from '../check_es_version';
import { ensureEsVersion } from '../ensure_es_version';
describe('plugins/elasticsearch', () => {
describe('lib/check_es_version', () => {
describe('lib/ensure_es_version', () => {
const KIBANA_VERSION = '5.1.0';
let server;
let plugin;
let callWithInternalUser;
beforeEach(function () {
server = {
log: sinon.stub(),
plugins: {
elasticsearch: {
client: {
nodes: {}
},
getCluster: sinon.stub().withArgs('admin').returns({ callWithInternalUser: sinon.stub() }),
status: {
red: sinon.stub()
},
@ -52,25 +51,27 @@ describe('plugins/elasticsearch', () => {
nodes[name] = node;
}
const client = server.plugins.elasticsearch.client;
client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes }));
const cluster = server.plugins.elasticsearch.getCluster('admin');
cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns(Promise.resolve({ nodes: nodes }));
callWithInternalUser = cluster.callWithInternalUser;
}
function setNodeWithoutHTTP(version) {
const nodes = { 'node-without-http': { version, ip: 'ip' } };
const client = server.plugins.elasticsearch.client;
client.nodes.info = sinon.stub().returns(Promise.resolve({ nodes: nodes }));
const cluster = server.plugins.elasticsearch.getCluster('admin');
cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns(Promise.resolve({ nodes: nodes }));
callWithInternalUser = cluster.callWithInternalUser;
}
it('returns true with single a node that matches', async () => {
setNodes('5.1.0');
const result = await checkEsVersion(server, KIBANA_VERSION);
const result = await ensureEsVersion(server, KIBANA_VERSION);
expect(result).to.be(true);
});
it('returns true with multiple nodes that satisfy', async () => {
setNodes('5.1.0', '5.2.0', '5.1.1-Beta1');
const result = await checkEsVersion(server, KIBANA_VERSION);
const result = await ensureEsVersion(server, KIBANA_VERSION);
expect(result).to.be(true);
});
@ -78,7 +79,7 @@ describe('plugins/elasticsearch', () => {
// 5.0.0 ES is too old to work with a 5.1.0 version of Kibana.
setNodes('5.1.0', '5.2.0', '5.0.0');
try {
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
} catch (e) {
expect(e).to.be.a(Error);
}
@ -91,7 +92,7 @@ describe('plugins/elasticsearch', () => {
{ version: '5.0.0', attributes: { client: 'true' } },
);
try {
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
} catch (e) {
expect(e).to.be.a(Error);
}
@ -99,7 +100,7 @@ describe('plugins/elasticsearch', () => {
it('warns if a node is only off by a patch version', async () => {
setNodes('5.1.1');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 2);
expect(server.log.getCall(0).args[0]).to.contain('debug');
expect(server.log.getCall(1).args[0]).to.contain('warning');
@ -107,7 +108,7 @@ describe('plugins/elasticsearch', () => {
it('warns if a node is off by a patch version and without http publish address', async () => {
setNodeWithoutHTTP('5.1.1');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 2);
expect(server.log.getCall(0).args[0]).to.contain('debug');
expect(server.log.getCall(1).args[0]).to.contain('warning');
@ -116,7 +117,7 @@ describe('plugins/elasticsearch', () => {
it('errors if a node incompatible and without http publish address', async () => {
setNodeWithoutHTTP('6.1.1');
try {
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
} catch (e) {
expect(e.message).to.contain('incompatible nodes');
expect(e).to.be.a(Error);
@ -126,12 +127,12 @@ describe('plugins/elasticsearch', () => {
it('only warns once per node list', async () => {
setNodes('5.1.1');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 2);
expect(server.log.getCall(0).args[0]).to.contain('debug');
expect(server.log.getCall(1).args[0]).to.contain('warning');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 3);
expect(server.log.getCall(2).args[0]).to.contain('debug');
});
@ -139,13 +140,13 @@ describe('plugins/elasticsearch', () => {
it('warns again if the node list changes', async () => {
setNodes('5.1.1');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 2);
expect(server.log.getCall(0).args[0]).to.contain('debug');
expect(server.log.getCall(1).args[0]).to.contain('warning');
setNodes('5.1.2');
await checkEsVersion(server, KIBANA_VERSION);
await ensureEsVersion(server, KIBANA_VERSION);
sinon.assert.callCount(server.log, 4);
expect(server.log.getCall(2).args[0]).to.contain('debug');
expect(server.log.getCall(3).args[0]).to.contain('warning');

View file

@ -0,0 +1,48 @@
import expect from 'expect.js';
import { noop } from 'lodash';
import sinon from 'sinon';
import { ensureNotTribe } from '../ensure_not_tribe';
describe('plugins/elasticsearch ensureNotTribe', () => {
const sandbox = sinon.sandbox.create();
afterEach(() => sandbox.restore());
const stubcallWithInternalUser = (nodesInfoResp = { nodes: {} }) => {
return sinon.stub().withArgs(
'nodes.info',
sinon.match.any
).returns(
Promise.resolve(nodesInfoResp)
);
};
it('fetches the local node stats of the node that the elasticsearch client is connected to', async () => {
const callWithInternalUser = stubcallWithInternalUser();
await ensureNotTribe(callWithInternalUser);
sinon.assert.calledOnce(callWithInternalUser);
});
it('throws a SetupError when the node info contains tribe settings', async () => {
const nodeInfo = {
nodes: {
__nodeId__: {
settings: {
tribe: {
t1: {},
t2: {},
}
}
}
}
};
try {
await ensureNotTribe(stubcallWithInternalUser(nodeInfo));
throw new Error('ensureNotTribe() should have thrown');
} catch (err) {
expect(err).to.be.a(Error);
}
});
});

View file

@ -13,10 +13,12 @@ const esPort = serverConfig.servers.elasticsearch.port;
const esUrl = url.format(serverConfig.servers.elasticsearch);
describe('plugins/elasticsearch', () => {
describe('lib/health_check', () => {
describe('lib/health_check', function () {
this.timeout(3000);
let health;
let plugin;
let client;
let cluster;
beforeEach(() => {
const COMPATIBLE_VERSION_NUMBER = '5.0.0';
@ -34,19 +36,11 @@ describe('plugins/elasticsearch', () => {
}
};
// set up the elasticsearch client stub
client = {
cluster: { health: sinon.stub() },
indices: { create: sinon.stub() },
nodes: { info: sinon.stub() },
ping: sinon.stub(),
create: sinon.stub(),
index: sinon.stub().returns(Promise.resolve()),
get: sinon.stub().returns(Promise.resolve({ found: false })),
search: sinon.stub().returns(Promise.resolve({ hits: { hits: [] } })),
};
client.nodes.info.returns(Promise.resolve({
cluster = { callWithInternalUser: sinon.stub() };
cluster.callWithInternalUser.withArgs('index', sinon.match.any).returns(Promise.resolve());
cluster.callWithInternalUser.withArgs('get', sinon.match.any).returns(Promise.resolve({ found: false }));
cluster.callWithInternalUser.withArgs('search', sinon.match.any).returns(Promise.resolve({ hits: { hits: [] } }));
cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any).returns(Promise.resolve({
nodes: {
'node-01': {
version: COMPATIBLE_VERSION_NUMBER,
@ -68,7 +62,11 @@ describe('plugins/elasticsearch', () => {
log: sinon.stub(),
info: { port: 5601 },
config: function () { return { get, set }; },
plugins: { elasticsearch: { client } }
plugins: {
elasticsearch: {
getCluster: sinon.stub().returns(cluster)
}
}
};
health = healthCheck(plugin, server);
@ -79,44 +77,59 @@ describe('plugins/elasticsearch', () => {
});
it('should set the cluster green if everything is ready', function () {
client.ping.returns(Promise.resolve());
client.cluster.health.returns(Promise.resolve({ timed_out: false, status: 'green' }));
cluster.callWithInternalUser.withArgs('ping').returns(Promise.resolve());
cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(
Promise.resolve({ timed_out: false, status: 'green' })
);
return health.run()
.then(function () {
sinon.assert.calledOnce(plugin.status.yellow);
expect(plugin.status.yellow.args[0][0]).to.be('Waiting for Elasticsearch');
sinon.assert.calledOnce(client.ping);
sinon.assert.calledOnce(client.nodes.info);
sinon.assert.calledOnce(client.cluster.health);
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping'));
sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any));
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any));
sinon.assert.calledOnce(plugin.status.green);
expect(plugin.status.green.args[0][0]).to.be('Kibana index ready');
});
});
it('should set the cluster red if the ping fails, then to green', function () {
client.ping.onCall(0).returns(Promise.reject(new NoConnections()));
client.ping.onCall(1).returns(Promise.resolve());
client.cluster.health.returns(Promise.resolve({ timed_out: false, status: 'green' }));
const ping = cluster.callWithInternalUser.withArgs('ping');
ping.onCall(0).returns(Promise.reject(new NoConnections()));
ping.onCall(1).returns(Promise.resolve());
cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any).returns(
Promise.resolve({ timed_out: false, status: 'green' })
);
return health.run()
.then(function () {
sinon.assert.calledOnce(plugin.status.yellow);
expect(plugin.status.yellow.args[0][0]).to.be('Waiting for Elasticsearch');
sinon.assert.calledOnce(plugin.status.red);
expect(plugin.status.red.args[0][0]).to.be(
`Unable to connect to Elasticsearch at ${esUrl}.`
);
sinon.assert.calledTwice(client.ping);
sinon.assert.calledOnce(client.nodes.info);
sinon.assert.calledOnce(client.cluster.health);
sinon.assert.calledTwice(ping);
sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any));
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any));
sinon.assert.calledOnce(plugin.status.green);
expect(plugin.status.green.args[0][0]).to.be('Kibana index ready');
});
});
it('should set the cluster red if the health check status is red, then to green', function () {
client.ping.returns(Promise.resolve());
client.cluster.health.onCall(0).returns(Promise.resolve({ timed_out: false, status: 'red' }));
client.cluster.health.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' }));
cluster.callWithInternalUser.withArgs('ping').returns(Promise.resolve());
const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any);
clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: false, status: 'red' }));
clusterHealth.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' }));
return health.run()
.then(function () {
sinon.assert.calledOnce(plugin.status.yellow);
@ -125,39 +138,45 @@ describe('plugins/elasticsearch', () => {
expect(plugin.status.red.args[0][0]).to.be(
'Elasticsearch is still initializing the kibana index.'
);
sinon.assert.calledOnce(client.ping);
sinon.assert.calledOnce(client.nodes.info);
sinon.assert.calledTwice(client.cluster.health);
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping'));
sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any));
sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any));
sinon.assert.calledOnce(plugin.status.green);
expect(plugin.status.green.args[0][0]).to.be('Kibana index ready');
});
});
it('should set the cluster yellow if the health check timed_out and create index', function () {
client.ping.returns(Promise.resolve());
client.cluster.health.onCall(0).returns(Promise.resolve({ timed_out: true, status: 'red' }));
client.cluster.health.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' }));
client.indices.create.returns(Promise.resolve());
cluster.callWithInternalUser.withArgs('ping').returns(Promise.resolve());
const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any);
clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: true, status: 'red' }));
clusterHealth.onCall(1).returns(Promise.resolve({ timed_out: false, status: 'green' }));
cluster.callWithInternalUser.withArgs('indices.create', sinon.match.any).returns(Promise.resolve());
return health.run()
.then(function () {
sinon.assert.calledTwice(plugin.status.yellow);
expect(plugin.status.yellow.args[0][0]).to.be('Waiting for Elasticsearch');
expect(plugin.status.yellow.args[1][0]).to.be('No existing Kibana index found');
sinon.assert.calledOnce(client.ping);
sinon.assert.calledOnce(client.indices.create);
sinon.assert.calledOnce(client.nodes.info);
sinon.assert.calledTwice(client.cluster.health);
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('ping'));
sinon.assert.calledOnce(cluster.callWithInternalUser.withArgs('indices.create', sinon.match.any));
sinon.assert.calledTwice(cluster.callWithInternalUser.withArgs('nodes.info', sinon.match.any));
sinon.assert.calledTwice(clusterHealth);
});
});
describe('#waitUntilReady', function () {
it('polls health until index is ready', function () {
client.cluster.health.onCall(0).returns(Promise.resolve({ timed_out: true })); // no index
client.cluster.health.onCall(1).returns(Promise.resolve({ status: 'red' })); // initializing
client.cluster.health.onCall(2).returns(Promise.resolve({ status: 'green' })); // ready
const clusterHealth = cluster.callWithInternalUser.withArgs('cluster.health', sinon.match.any);
clusterHealth.onCall(0).returns(Promise.resolve({ timed_out: true }));
clusterHealth.onCall(1).returns(Promise.resolve({ status: 'red' }));
clusterHealth.onCall(2).returns(Promise.resolve({ status: 'green' }));
return health.waitUntilReady().then(function () {
sinon.assert.calledThrice(client.cluster.health);
sinon.assert.calledThrice(clusterHealth);
});
});
});

View file

@ -2,20 +2,25 @@ import expect from 'expect.js';
import mapUri from '../map_uri';
import { get, defaults } from 'lodash';
import sinon from 'sinon';
import url from 'url';
describe('plugins/elasticsearch', function () {
describe('lib/map_uri', function () {
let request;
function stubServer(settings) {
const values = defaults(settings || {}, {
'elasticsearch.url': 'http://localhost:9200',
'elasticsearch.requestHeadersWhitelist': ['authorization'],
'elasticsearch.customHeaders': {}
function stubCluster(settings) {
settings = defaults(settings || {}, {
url: 'http://localhost:9200',
requestHeadersWhitelist: ['authorization'],
customHeaders: {}
});
const config = { get: (key, def) => get(values, key, def) };
return { config: () => config };
return {
getUrl: () => settings.url,
getCustomHeaders: () => settings.customHeaders,
getRequestHeadersWhitelist: () => settings.requestHeadersWhitelist
};
}
beforeEach(function () {
@ -34,34 +39,34 @@ describe('plugins/elasticsearch', function () {
});
it('sends custom headers if set', function () {
const server = stubServer({
'elasticsearch.customHeaders': { foo: 'bar' }
});
const settings = {
customHeaders: { foo: 'bar' }
};
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamHeaders).to.have.property('foo', 'bar');
});
});
it('sends configured custom headers even if the same named header exists in request', function () {
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': ['x-my-custom-header'],
'elasticsearch.customHeaders': { 'x-my-custom-header': 'asconfigured' }
});
const settings = {
requestHeadersWhitelist: ['x-my-custom-header'],
customHeaders: { 'x-my-custom-header': 'asconfigured' }
};
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamHeaders).to.have.property('x-my-custom-header', 'asconfigured');
});
});
it('only proxies the whitelisted request headers', function () {
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': ['x-my-custom-HEADER', 'Authorization'],
});
const settings = {
requestHeadersWhitelist: ['x-my-custom-HEADER', 'Authorization'],
};
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamHeaders).to.have.property('authorization');
expect(upstreamHeaders).to.have.property('x-my-custom-header');
@ -70,24 +75,24 @@ describe('plugins/elasticsearch', function () {
});
it('proxies no headers if whitelist is set to []', function () {
const server = stubServer({
'elasticsearch.requestHeadersWhitelist': [],
});
const settings = {
requestHeadersWhitelist: [],
};
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(Object.keys(upstreamHeaders).length).to.be(0);
});
});
it('proxies no headers if whitelist is set to no value', function () {
const server = stubServer({
const settings = {
// joi converts `elasticsearch.requestHeadersWhitelist: null` into
// an array with a null inside because of the `array().single()` rule.
'elasticsearch.requestHeadersWhitelist': [ null ],
});
requestHeadersWhitelist: [ null ],
};
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(Object.keys(upstreamHeaders).length).to.be(0);
});
@ -95,9 +100,8 @@ describe('plugins/elasticsearch', function () {
it('strips the /elasticsearch prefix from the path', () => {
request.path = '/elasticsearch/es/path';
const server = stubServer();
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('http://localhost:9200/es/path');
});
@ -105,9 +109,9 @@ describe('plugins/elasticsearch', function () {
it('extends the es.url path', function () {
request.path = '/elasticsearch/index/type';
const server = stubServer({ 'elasticsearch.url': 'https://localhost:9200/base-path' });
const settings = { url: 'https://localhost:9200/base-path' };
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('https://localhost:9200/base-path/index/type');
});
@ -116,9 +120,9 @@ describe('plugins/elasticsearch', function () {
it('extends the es.url query string', function () {
request.path = '/elasticsearch/*';
request.query = { foo: 'bar' };
const server = stubServer({ 'elasticsearch.url': 'https://localhost:9200/?base=query' });
const settings = { url: 'https://localhost:9200/?base=query' };
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(settings), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('https://localhost:9200/*?foo=bar&base=query');
});
@ -127,9 +131,8 @@ describe('plugins/elasticsearch', function () {
it('filters the _ querystring param', function () {
request.path = '/elasticsearch/*';
request.query = { _: Date.now() };
const server = stubServer();
mapUri(server)(request, function (err, upstreamUri, upstreamHeaders) {
mapUri(stubCluster(), '/elasticsearch')(request, function (err, upstreamUri, upstreamHeaders) {
expect(err).to.be(null);
expect(upstreamUri).to.be('http://localhost:9200/*');
});

View file

@ -9,7 +9,7 @@ describe('plugins/elasticsearch', function () {
describe('lib/upgrade_config', function () {
let get;
let server;
let client;
let callWithInternalUser;
let config;
let upgrade;
@ -18,7 +18,9 @@ describe('plugins/elasticsearch', function () {
get.withArgs('kibana.index').returns('.my-kibana');
get.withArgs('pkg.version').returns('4.0.1');
get.withArgs('pkg.buildNum').returns(Math.random());
client = { create: sinon.stub() };
callWithInternalUser = sinon.stub();
server = {
log: sinon.stub(),
config: function () {
@ -26,7 +28,13 @@ describe('plugins/elasticsearch', function () {
get: get
};
},
plugins: { elasticsearch: { client: client } }
plugins: {
elasticsearch: {
getCluster: sinon.stub().withArgs('admin').returns({
callWithInternalUser: callWithInternalUser
})
}
}
};
upgrade = upgradeConfig(server);
});
@ -35,7 +43,7 @@ describe('plugins/elasticsearch', function () {
const response = { hits: { hits:[] } };
beforeEach(function () {
client.create.returns(Promise.resolve());
callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve());
});
describe('production', function () {
@ -47,15 +55,15 @@ describe('plugins/elasticsearch', function () {
it('should resolve buildNum to pkg.buildNum config', function () {
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(client.create);
const params = client.create.args[0][0];
sinon.assert.calledOnce(callWithInternalUser);
const params = callWithInternalUser.args[0][1];
expect(params.body).to.have.property('buildNum', get('pkg.buildNum'));
});
});
it('should resolve version to pkg.version config', function () {
return upgrade(response).then(function (resp) {
const params = client.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params).to.have.property('id', get('pkg.version'));
});
});
@ -70,14 +78,14 @@ describe('plugins/elasticsearch', function () {
it('should resolve buildNum to pkg.buildNum config', function () {
return upgrade(response).then(function (resp) {
const params = client.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params.body).to.have.property('buildNum', get('pkg.buildNum'));
});
});
it('should resolve version to pkg.version config', function () {
return upgrade(response).then(function (resp) {
const params = client.create.args[0][0];
const params = callWithInternalUser.args[0][1];
expect(params).to.have.property('id', get('pkg.version'));
});
});
@ -93,11 +101,12 @@ describe('plugins/elasticsearch', function () {
it('should create new config if the nothing is upgradeable', function () {
get.withArgs('pkg.buildNum').returns(9833);
client.create.returns(Promise.resolve());
callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve());
const response = { hits: { hits: [ { _id: '4.0.1-alpha3' }, { _id: '4.0.1-beta1' }, { _id: '4.0.0-SNAPSHOT1' } ] } };
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(client.create);
const params = client.create.args[0][0];
sinon.assert.calledOnce(callWithInternalUser);
const params = callWithInternalUser.args[0][1];
expect(params).to.have.property('body');
expect(params.body).to.have.property('buildNum', 9833);
expect(params).to.have.property('index', '.my-kibana');
@ -108,11 +117,13 @@ describe('plugins/elasticsearch', function () {
it('should update the build number on the new config', function () {
get.withArgs('pkg.buildNum').returns(5801);
client.create.returns(Promise.resolve());
callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve());
const response = { hits: { hits: [ { _id: '4.0.0', _source: { buildNum: 1 } } ] } };
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(client.create);
const params = client.create.args[0][0];
sinon.assert.calledOnce(callWithInternalUser);
const params = callWithInternalUser.args[0][1];
expect(params).to.have.property('body');
expect(params.body).to.have.property('buildNum', 5801);
expect(params).to.have.property('index', '.my-kibana');
@ -123,8 +134,10 @@ describe('plugins/elasticsearch', function () {
it('should log a message for upgrades', function () {
get.withArgs('pkg.buildNum').returns(5801);
client.create.returns(Promise.resolve());
callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve());
const response = { hits: { hits: [ { _id: '4.0.0', _source: { buildNum: 1 } } ] } };
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(server.log);
expect(server.log.args[0][0]).to.eql(['plugin', 'elasticsearch']);
@ -137,11 +150,13 @@ describe('plugins/elasticsearch', function () {
it('should copy attributes from old config', function () {
get.withArgs('pkg.buildNum').returns(5801);
client.create.returns(Promise.resolve());
callWithInternalUser.withArgs('create', sinon.match.any).returns(Promise.resolve());
const response = { hits: { hits: [ { _id: '4.0.0', _source: { buildNum: 1, defaultIndex: 'logstash-*' } } ] } };
return upgrade(response).then(function (resp) {
sinon.assert.calledOnce(client.create);
const params = client.create.args[0][0];
sinon.assert.calledOnce(callWithInternalUser);
const params = callWithInternalUser.args[0][1];
expect(params).to.have.property('body');
expect(params.body).to.have.property('defaultIndex', 'logstash-*');
});

View file

@ -1,31 +0,0 @@
import _ from 'lodash';
import Promise from 'bluebird';
import Boom from 'boom';
import toPath from 'lodash/internal/toPath';
import filterHeaders from './filter_headers';
module.exports = (server, client) => {
return (req, endpoint, clientParams = {}, options = {}) => {
const wrap401Errors = options.wrap401Errors !== false;
const filteredHeaders = filterHeaders(req.headers, server.config().get('elasticsearch.requestHeadersWhitelist'));
_.set(clientParams, 'headers', filteredHeaders);
const path = toPath(endpoint);
const api = _.get(client, path);
let apiContext = _.get(client, path.slice(0, -1));
if (_.isEmpty(apiContext)) {
apiContext = client;
}
if (!api) throw new Error(`callWithRequest called with an invalid endpoint: ${endpoint}`);
return api.call(apiContext, clientParams)
.catch((err) => {
if (!wrap401Errors || err.statusCode !== 401) {
return Promise.reject(err);
}
const boomError = Boom.wrap(err, err.statusCode);
const wwwAuthHeader = _.get(err, 'body.error.header[WWW-Authenticate]');
boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"';
throw boomError;
});
};
};

View file

@ -0,0 +1,39 @@
export function clientLogger(server) {
return class ElasticsearchClientLogging {
// additional tags to differentiate connection
tags = [];
logQueries = false;
error(err) {
server.log(['error', 'elasticsearch'].concat(this.tags), err);
}
warning(message) {
server.log(['warning', 'elasticsearch'].concat(this.tags), message);
}
trace(method, options, query, _response, statusCode) {
/* Check if query logging is enabled
* It requires Kibana to be configured with verbose logging turned on. */
if (this.logQueries) {
const methodAndPath = `${method} ${options.path}`;
const queryDsl = query ? query.trim() : '';
server.log(['elasticsearch', 'query', 'debug'].concat(this.tags), [
statusCode,
methodAndPath,
queryDsl
].join('\n'));
}
}
// elasticsearch-js expects the following functions to exist
info() {}
debug() {}
close() {}
};
}

View file

@ -0,0 +1,110 @@
import elasticsearch from 'elasticsearch';
import { get, set, isEmpty, cloneDeep, pick } from 'lodash';
import toPath from 'lodash/internal/toPath';
import Boom from 'boom';
import filterHeaders from './filter_headers';
import { parseConfig } from './parse_config';
export class Cluster {
constructor(config) {
this._config = Object.assign({}, config);
this.errors = elasticsearch.errors;
this._client = this.createClient();
this._noAuthClient = this.createClient({ auth: false });
return this;
}
callWithRequest = (req = {}, endpoint, clientParams = {}, options = {}) => {
if (req.headers) {
const filteredHeaders = filterHeaders(req.headers, this.getRequestHeadersWhitelist());
set(clientParams, 'headers', filteredHeaders);
}
return callAPI(this._noAuthClient, endpoint, clientParams, options);
}
callWithInternalUser = (endpoint, clientParams = {}, options = {}) => {
return callAPI(this._client, endpoint, clientParams, options);
}
getRequestHeadersWhitelist = () => getClonedProperty(this._config, 'requestHeadersWhitelist');
getCustomHeaders = () => getClonedProperty(this._config, 'customHeaders');
getRequestTimeout = () => getClonedProperty(this._config, 'requestTimeout');
getUrl = () => getClonedProperty(this._config, 'url');
getSsl = () => getClonedProperty(this._config, 'ssl');
getClient = () => this._client;
close() {
if (this._client) {
this._client.close();
}
if (this._noAuthClient) {
this._noAuthClient.close();
}
}
createClient = configOverrides => {
const config = Object.assign({}, this._getClientConfig(), configOverrides);
return new elasticsearch.Client(parseConfig(config));
}
_getClientConfig = () => {
return getClonedProperties(this._config, [
'url',
'ssl',
'username',
'password',
'customHeaders',
'plugins',
'apiVersion',
'keepAlive',
'pingTimeout',
'requestTimeout',
'log'
]);
}
}
function callAPI(client, endpoint, clientParams = {}, options = {}) {
const wrap401Errors = options.wrap401Errors !== false;
const clientPath = toPath(endpoint);
const api = get(client, clientPath);
let apiContext = get(client, clientPath.slice(0, -1));
if (isEmpty(apiContext)) {
apiContext = client;
}
if (!api) {
throw new Error(`called with an invalid endpoint: ${endpoint}`);
}
return api.call(apiContext, clientParams).catch((err) => {
if (!wrap401Errors || err.statusCode !== 401) {
return Promise.reject(err);
}
const boomError = Boom.wrap(err, err.statusCode);
const wwwAuthHeader = get(err, 'body.error.header[WWW-Authenticate]');
boomError.output.headers['WWW-Authenticate'] = wwwAuthHeader || 'Basic realm="Authorization Required"';
throw boomError;
});
}
function getClonedProperties(config, paths) {
return cloneDeep(paths ? pick(config, paths) : config);
}
function getClonedProperty(config, path) {
return cloneDeep(path ? get(config, path) : config);
}

View file

@ -0,0 +1,19 @@
import { bindKey } from 'lodash';
import { clientLogger } from './client_logger';
export function createAdminCluster(server) {
const config = server.config();
const ElasticsearchClientLogging = clientLogger(server);
class AdminClientLogging extends ElasticsearchClientLogging {
tags = ['admin'];
logQueries = config.get('elasticsearch.logQueries');
}
const adminCluster = server.plugins.elasticsearch.createCluster(
'admin',
Object.assign({ log: AdminClientLogging }, config.get('elasticsearch'))
);
server.on('close', bindKey(adminCluster, 'close'));
}

View file

@ -1,32 +1,15 @@
import url from 'url';
import _ from 'lodash';
import { get, size } from 'lodash';
const readFile = (file) => require('fs').readFileSync(file, 'utf8');
import http from 'http';
import https from 'https';
module.exports = _.memoize(function (server) {
const config = server.config();
const target = url.parse(config.get('elasticsearch.url'));
import { parseConfig } from './parse_config';
export default function (config) {
const target = url.parse(get(config, 'url'));
if (!/^https/.test(target.protocol)) return new http.Agent();
const agentOptions = {
rejectUnauthorized: config.get('elasticsearch.ssl.verify')
};
if (_.size(config.get('elasticsearch.ssl.ca'))) {
agentOptions.ca = config.get('elasticsearch.ssl.ca').map(readFile);
}
// Add client certificate and key if required by elasticsearch
if (config.get('elasticsearch.ssl.cert') && config.get('elasticsearch.ssl.key')) {
agentOptions.cert = readFile(config.get('elasticsearch.ssl.cert'));
agentOptions.key = readFile(config.get('elasticsearch.ssl.key'));
}
return new https.Agent(agentOptions);
});
// See https://lodash.com/docs#memoize: We use a Map() instead of the default, because we want the keys in the cache
// to be the server objects, and by default these would be coerced to strings as keys (which wouldn't be useful)
module.exports.cache = new Map();
return new https.Agent(parseConfig(config).ssl);
}

View file

@ -0,0 +1,25 @@
import { Cluster } from './cluster';
import { get, set } from 'lodash';
export function createClusters(server) {
const esPlugin = server.plugins.elasticsearch;
esPlugin._clusters = esPlugin._clusters || new Map();
return {
get(name) {
return esPlugin._clusters.get(name);
},
create(name, config) {
const cluster = new Cluster(config);
if (esPlugin._clusters.has(name)) {
throw new Error(`cluster '${name}' already exists`);
}
esPlugin._clusters.set(name, cluster);
return cluster;
}
};
}

View file

@ -0,0 +1,27 @@
import { bindKey } from 'lodash';
import { clientLogger } from './client_logger';
export function createDataCluster(server) {
const config = server.config();
const ElasticsearchClientLogging = clientLogger(server);
class DataClientLogging extends ElasticsearchClientLogging {
tags = ['data'];
logQueries = getConfig().logQueries;
}
function getConfig() {
if (Boolean(config.get('elasticsearch.tribe.url'))) {
return config.get('elasticsearch.tribe');
}
return config.get('elasticsearch');
}
const dataCluster = server.plugins.elasticsearch.createCluster(
'data',
Object.assign({ log: DataClientLogging }, getConfig())
);
server.on('close', bindKey(dataCluster, 'close'));
}

View file

@ -2,10 +2,10 @@ import { format } from 'util';
import { mappings } from './kibana_index_mappings';
module.exports = function (server) {
const client = server.plugins.elasticsearch.client;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const index = server.config().get('kibana.index');
return client.indices.create({
return callWithInternalUser('indices.create', {
index: index,
body: {
settings: {
@ -18,7 +18,7 @@ module.exports = function (server) {
throw new Error(`Unable to create Kibana index "${index}"`);
})
.then(function () {
return client.cluster.health({
return callWithInternalUser('cluster.health', {
waitForStatus: 'yellow',
index: index
})

View file

@ -3,48 +3,60 @@ import mapUri from './map_uri';
import { resolve } from 'url';
import { assign } from 'lodash';
function createProxy(server, method, route, config) {
function createProxy(server, method, path, config) {
const proxies = new Map([
['/elasticsearch', server.plugins.elasticsearch.getCluster('data')],
['/es_admin', server.plugins.elasticsearch.getCluster('admin')]
]);
const options = {
method: method,
path: createProxy.createPath(route),
config: {
timeout: {
socket: server.config().get('elasticsearch.requestTimeout')
}
},
handler: {
proxy: {
mapUri: mapUri(server),
agent: createAgent(server),
xforward: true,
timeout: server.config().get('elasticsearch.requestTimeout'),
onResponse: function (err, responseFromUpstream, request, reply) {
if (err) {
reply(err);
return;
}
const responseHandler = function (err, upstreamResponse, request, reply) {
if (err) {
reply(err);
return;
}
if (responseFromUpstream.headers.location) {
// TODO: Workaround for #8705 until hapi has been updated to >= 15.0.0
responseFromUpstream.headers.location = encodeURI(responseFromUpstream.headers.location);
}
if (upstreamResponse.headers.location) {
// TODO: Workaround for #8705 until hapi has been updated to >= 15.0.0
upstreamResponse.headers.location = encodeURI(upstreamResponse.headers.location);
}
reply(null, responseFromUpstream);
}
}
},
reply(null, upstreamResponse);
};
assign(options.config, config);
for (const [proxyPrefix, cluster] of proxies) {
const options = {
method,
path: createProxy.createPath(proxyPrefix, path),
config: {
timeout: {
socket: cluster.getRequestTimeout()
}
},
handler: {
proxy: {
mapUri: mapUri(cluster, proxyPrefix),
agent: createAgent({
url: cluster.getUrl(),
ssl: cluster.getSsl()
}),
xforward: true,
timeout: cluster.getRequestTimeout(),
onResponse: responseHandler
}
},
};
server.route(options);
assign(options.config, config);
server.route(options);
}
}
createProxy.createPath = function createPath(path) {
const pre = '/elasticsearch';
const sep = path[0] === '/' ? '' : '/';
return `${pre}${sep}${path}`;
createProxy.createPath = function createPath(prefix, path) {
path = path[0] === '/' ? path : `/${path}`;
prefix = prefix[0] === '/' ? prefix : `/${prefix}`;
return `${prefix}${path}`;
};
module.exports = createProxy;

View file

@ -3,25 +3,30 @@
* that defined in Kibana's package.json.
*/
import _ from 'lodash';
import { forEach, get } from 'lodash';
import isEsCompatibleWithKibana from './is_es_compatible_with_kibana';
/**
* tracks the node descriptions that get logged in warnings so
* that we don't spam the log with the same message over and over.
* tracks the node descriptions that get logged in warnings so
* that we don't spam the log with the same message over and over.
*
* There are situations, like in testing or multi-tenancy, where
* the server argument changes, so we must track the previous
* node warnings per server
* There are situations, like in testing or multi-tenancy, where
* the server argument changes, so we must track the previous
* node warnings per server
*/
const lastWarnedNodesForServer = new WeakMap();
module.exports = function checkEsVersion(server, kibanaVersion) {
export function ensureEsVersion(server, kibanaVersion) {
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
server.log(['plugin', 'debug'], 'Checking Elasticsearch version');
const client = server.plugins.elasticsearch.client;
return client.nodes.info()
return callWithInternalUser('nodes.info', {
filterPath: [
'nodes.*.version',
'nodes.*.http.publish_address',
'nodes.*.ip',
]
})
.then(function (info) {
// Aggregate incompatible ES nodes.
const incompatibleNodes = [];
@ -29,7 +34,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
// Aggregate ES nodes which should prompt a Kibana upgrade.
const warningNodes = [];
_.forEach(info.nodes, esNode => {
forEach(info.nodes, esNode => {
if (!isEsCompatibleWithKibana(esNode.version, kibanaVersion)) {
// Exit early to avoid collecting ES nodes with newer major versions in the `warningNodes`.
return incompatibleNodes.push(esNode);
@ -44,7 +49,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
function getHumanizedNodeNames(nodes) {
return nodes.map(node => {
const publishAddress = _.get(node, 'http.publish_address') ? (_.get(node, 'http.publish_address') + ' ') : '';
const publishAddress = get(node, 'http.publish_address') ? (get(node, 'http.publish_address') + ' ') : '';
return 'v' + node.version + ' @ ' + publishAddress + '(' + node.ip + ')';
});
}
@ -53,7 +58,7 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
const simplifiedNodes = warningNodes.map(node => ({
version: node.version,
http: {
publish_address: _.get(node, 'http.publish_address')
publish_address: get(node, 'http.publish_address')
},
ip: node.ip,
}));
@ -85,4 +90,4 @@ module.exports = function checkEsVersion(server, kibanaVersion) {
return true;
});
};
}

View file

@ -0,0 +1,18 @@
import { get } from 'lodash';
export function ensureNotTribe(callWithInternalUser) {
return callWithInternalUser('nodes.info', {
nodeId: '_local',
filterPath: 'nodes.*.settings.tribe'
})
.then(function (info) {
const nodeId = Object.keys(info.nodes || {})[0];
const tribeSettings = get(info, ['nodes', nodeId, 'settings', 'tribe']);
if (tribeSettings) {
throw new Error('Kibana does not support using tribe nodes as the primary elasticsearch connection.');
}
return true;
});
}

View file

@ -1,99 +0,0 @@
import elasticsearch from 'elasticsearch';
import _ from 'lodash';
import Bluebird from 'bluebird';
const readFile = (file) => require('fs').readFileSync(file, 'utf8');
import util from 'util';
import url from 'url';
import callWithRequest from './call_with_request';
import filterHeaders from './filter_headers';
module.exports = function (server) {
const config = server.config();
class ElasticsearchClientLogging {
error(err) {
server.log(['error', 'elasticsearch'], err);
}
warning(message) {
server.log(['warning', 'elasticsearch'], message);
}
info() {}
debug() {}
trace() {}
close() {}
}
function createClient(options) {
options = _.defaults(options || {}, {
url: config.get('elasticsearch.url'),
username: config.get('elasticsearch.username'),
password: config.get('elasticsearch.password'),
verifySsl: config.get('elasticsearch.ssl.verify'),
clientCrt: config.get('elasticsearch.ssl.cert'),
clientKey: config.get('elasticsearch.ssl.key'),
ca: config.get('elasticsearch.ssl.ca'),
apiVersion: config.get('elasticsearch.apiVersion'),
pingTimeout: config.get('elasticsearch.pingTimeout'),
requestTimeout: config.get('elasticsearch.requestTimeout'),
keepAlive: true,
auth: true
});
const uri = url.parse(options.url);
let authorization;
if (options.auth && options.username && options.password) {
uri.auth = util.format('%s:%s', options.username, options.password);
}
const ssl = { rejectUnauthorized: options.verifySsl };
if (options.clientCrt && options.clientKey) {
ssl.cert = readFile(options.clientCrt);
ssl.key = readFile(options.clientKey);
}
if (options.ca) {
ssl.ca = options.ca.map(readFile);
}
const host = {
host: uri.hostname,
port: uri.port,
protocol: uri.protocol,
path: uri.pathname,
auth: uri.auth,
query: uri.query,
headers: config.get('elasticsearch.customHeaders')
};
return new elasticsearch.Client({
host,
ssl,
plugins: options.plugins,
apiVersion: options.apiVersion,
keepAlive: options.keepAlive,
pingTimeout: options.pingTimeout,
requestTimeout: options.requestTimeout,
defer: function () {
return Bluebird.defer();
},
log: ElasticsearchClientLogging
});
}
const client = createClient();
server.on('close', _.bindKey(client, 'close'));
const noAuthClient = createClient({ auth: false });
server.on('close', _.bindKey(noAuthClient, 'close'));
server.expose('ElasticsearchClientLogging', ElasticsearchClientLogging);
server.expose('client', client);
server.expose('createClient', createClient);
server.expose('callWithRequestFactory', _.partial(callWithRequest, server));
server.expose('callWithRequest', callWithRequest(server, noAuthClient));
server.expose('filterHeaders', filterHeaders);
server.expose('errors', elasticsearch.errors);
return client;
};

View file

@ -1,11 +1,11 @@
import _ from 'lodash';
import Promise from 'bluebird';
import elasticsearch from 'elasticsearch';
import exposeClient from './expose_client';
import migrateConfig from './migrate_config';
import createKibanaIndex from './create_kibana_index';
import checkEsVersion from './check_es_version';
import kibanaVersion from './kibana_version';
import { ensureEsVersion } from './ensure_es_version';
import { ensureNotTribe } from './ensure_not_tribe';
const NoConnections = elasticsearch.errors.NoConnections;
import util from 'util';
@ -15,27 +15,25 @@ const NO_INDEX = 'no_index';
const INITIALIZING = 'initializing';
const READY = 'ready';
const REQUEST_DELAY = 2500;
module.exports = function (plugin, server) {
const config = server.config();
const client = server.plugins.elasticsearch.client;
const callAdminAsKibanaUser = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;
const callDataAsKibanaUser = server.plugins.elasticsearch.getCluster('data').callWithInternalUser;
const REQUEST_DELAY = config.get('elasticsearch.healthCheck.delay');
plugin.status.yellow('Waiting for Elasticsearch');
function waitForPong() {
return client.ping().catch(function (err) {
function waitForPong(callWithInternalUser, url) {
return callWithInternalUser('ping').catch(function (err) {
if (!(err instanceof NoConnections)) throw err;
plugin.status.red(format('Unable to connect to Elasticsearch at %s.', url));
plugin.status.red(format('Unable to connect to Elasticsearch at %s.', config.get('elasticsearch.url')));
return Promise.delay(REQUEST_DELAY).then(waitForPong);
return Promise.delay(REQUEST_DELAY).then(waitForPong.bind(null, callWithInternalUser, url));
});
}
// just figure out the current "health" of the es setup
function getHealth() {
return client.cluster.health({
return callAdminAsKibanaUser('cluster.health', {
timeout: '5s', // tells es to not sit around and wait forever
index: config.get('kibana.index'),
ignore: [408]
@ -82,7 +80,7 @@ module.exports = function (plugin, server) {
}
function waitForEsVersion() {
return checkEsVersion(server, kibanaVersion.get()).catch(err => {
return ensureEsVersion(server, kibanaVersion.get()).catch(err => {
plugin.status.red(err);
return Promise.delay(REQUEST_DELAY).then(waitForEsVersion);
});
@ -93,14 +91,26 @@ module.exports = function (plugin, server) {
}
function check() {
return waitForPong()
.then(waitForEsVersion)
.then(waitForShards)
const healthCheck =
waitForPong(callAdminAsKibanaUser, config.get('elasticsearch.url'))
.then(waitForEsVersion)
.then(ensureNotTribe.bind(this, callAdminAsKibanaUser))
.then(waitForShards)
.then(_.partial(migrateConfig, server))
.then(() => {
const tribeUrl = config.get('elasticsearch.tribe.url');
if (tribeUrl) {
return waitForPong(callDataAsKibanaUser, tribeUrl)
.then(() => ensureEsVersion(server, kibanaVersion.get(), callDataAsKibanaUser));
}
});
return healthCheck
.then(setGreenStatus)
.then(_.partial(migrateConfig, server))
.catch(err => plugin.status.red(err));
}
let timeoutId = null;
function scheduleCheck(ms) {

View file

@ -3,9 +3,7 @@ import { parse as parseUrl, format as formatUrl, resolve } from 'url';
import filterHeaders from './filter_headers';
import setHeaders from './set_headers';
export default function mapUri(server, prefix) {
const config = server.config();
export default function mapUri(cluster, proxyPrefix) {
function joinPaths(pathA, pathB) {
return trimRight(pathA, '/') + '/' + trimLeft(pathB, '/');
}
@ -19,7 +17,7 @@ export default function mapUri(server, prefix) {
port: esUrlPort,
pathname: esUrlBasePath,
query: esUrlQuery
} = parseUrl(config.get('elasticsearch.url'), true);
} = parseUrl(cluster.getUrl(), true);
// copy most url components directly from the elasticsearch.url
const mappedUrlComponents = {
@ -31,17 +29,17 @@ export default function mapUri(server, prefix) {
};
// pathname
const reqSubPath = request.path.replace('/elasticsearch', '');
const reqSubPath = request.path.replace(proxyPrefix, '');
mappedUrlComponents.pathname = joinPaths(esUrlBasePath, reqSubPath);
// querystring
const mappedQuery = defaults(omit(request.query, '_'), esUrlQuery || {});
const mappedQuery = defaults(omit(request.query, '_'), esUrlQuery);
if (Object.keys(mappedQuery).length) {
mappedUrlComponents.query = mappedQuery;
}
const filteredHeaders = filterHeaders(request.headers, config.get('elasticsearch.requestHeadersWhitelist'));
const mappedHeaders = setHeaders(filteredHeaders, config.get('elasticsearch.customHeaders'));
const filteredHeaders = filterHeaders(request.headers, cluster.getRequestHeadersWhitelist());
const mappedHeaders = setHeaders(filteredHeaders, cluster.getCustomHeaders());
const mappedUrl = formatUrl(mappedUrlComponents);
done(null, mappedUrl, mappedHeaders);
};

View file

@ -3,7 +3,8 @@ import { mappings } from './kibana_index_mappings';
module.exports = function (server) {
const config = server.config();
const client = server.plugins.elasticsearch.client;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const options = {
index: config.get('kibana.index'),
type: 'config',
@ -20,5 +21,5 @@ module.exports = function (server) {
}
};
return client.search(options).then(upgrade(server));
return callWithInternalUser('search', options).then(upgrade(server));
};

View file

@ -0,0 +1,47 @@
import util from 'util';
import url from 'url';
import { get, size, pick } from 'lodash';
import { readFileSync } from 'fs';
import Bluebird from 'bluebird';
const readFile = (file) => readFileSync(file, 'utf8');
export function parseConfig(serverConfig = {}) {
const config = Object.assign({
keepAlive: true
}, pick(serverConfig, [
'plugins', 'apiVersion', 'keepAlive', 'pingTimeout',
'requestTimeout', 'log', 'logQueries'
]));
const uri = url.parse(serverConfig.url);
config.host = {
host: uri.hostname,
port: uri.port,
protocol: uri.protocol,
path: uri.pathname,
query: uri.query,
headers: serverConfig.customHeaders
};
// Auth
if (serverConfig.auth !== false && serverConfig.username && serverConfig.password) {
config.host.auth = util.format('%s:%s', serverConfig.username, serverConfig.password);
}
// SSL
config.ssl = { rejectUnauthorized: get(serverConfig, 'ssl.verify') };
if (get(serverConfig, 'ssl.cert') && get(serverConfig, 'ssl.key')) {
config.ssl.cert = readFile(serverConfig.ssl.cert);
config.ssl.key = readFile(serverConfig.ssl.key);
}
if (size(get(serverConfig, 'ssl.ca'))) {
config.ssl.ca = serverConfig.ssl.ca.map(readFile);
}
config.defer = () => Bluebird.defer();
return config;
}

View file

@ -6,11 +6,11 @@ import { format } from 'util';
module.exports = function (server) {
const MAX_INTEGER = Math.pow(2, 53) - 1;
const client = server.plugins.elasticsearch.client;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const config = server.config();
function createNewConfig() {
return client.create({
return callWithInternalUser('create', {
index: config.get('kibana.index'),
type: 'config',
body: { buildNum: config.get('pkg.buildNum') },
@ -31,7 +31,9 @@ module.exports = function (server) {
return hit._id !== '@@version' && hit._id === config.get('pkg.version');
});
if (devConfig) return Promise.resolve();
if (devConfig) {
return Promise.resolve();
}
// Look for upgradeable configs. If none of them are upgradeable
// then create a new one.
@ -50,7 +52,7 @@ module.exports = function (server) {
newVersion: config.get('pkg.version')
});
return client.create({
return callWithInternalUser('create', {
index: config.get('kibana.index'),
type: 'config',
body: body._source,

View file

@ -19,7 +19,6 @@ export default function HistogramVisType(Private) {
addTooltip: true,
addLegend: true,
legendPosition: 'right',
smoothLines: false,
scale: 'linear',
interpolate: 'linear',
mode: 'stacked',
@ -41,6 +40,16 @@ export default function HistogramVisType(Private) {
value: 'bottom',
text: 'bottom',
}],
interpolationModes: [{
value: 'linear',
text: 'straight',
}, {
value: 'cardinal',
text: 'smoothed',
}, {
value: 'step-after',
text: 'stepped',
}],
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],
editor: areaTemplate

View file

@ -0,0 +1,206 @@
<div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="colorSchema">
Color Schema
</label>
<div class="kuiSideBarFormRow__control">
<select
id="colorSchema"
class="kuiSelect kuiSideBarSelect"
ng-model="vis.params.colorSchema"
ng-options="mode for mode in vis.type.params.colorSchemas"
></select>
</div>
<div class="text-info text-center" ng-show="customColors" ng-click="resetColors()">reset colors</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="invertColors">
Reverse Color Schema
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="invertColors" type="checkbox" ng-model="vis.params.invertColors">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="axisScale">
Color Scale
</label>
<div class="kuiSideBarFormRow__control">
<select
id="axisScale"
class="kuiSelect kuiSideBarSelect"
ng-model="valueAxis.scale.type"
ng-options="mode for mode in vis.type.params.scales"
></select>
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="defaultYExtents">
Scale to Data Bounds
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="defaultYExtents" type="checkbox" ng-model="valueAxis.scale.defaultYExtents">
</div>
</div>
<div class="kuiSideBarFormRow" ng-if="!vis.params.setColorRange">
<label class="kuiSideBarFormRow__label" for="percentageMode">
Percentage Mode
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="percentageMode" type="checkbox" ng-model="vis.params.percentageMode">
</div>
</div>
<div class="kuiSideBarFormRow" ng-if="!vis.params.setColorRange">
<label class="kuiSideBarFormRow__label" for="colorsNumber">
Number of colors
</label>
<div class="kuiSideBarFormRow__control">
<input
id="colorsNumber"
class="kuiInput kuiSideBarInput"
ng-model="vis.params.colorsNumber"
type="number"
greater-than="1"
less-than="11"
>
</div>
</div>
<div>
<div class="kuiSideBarCollapsibleTitle">
<div
class="kuiSideBarCollapsibleTitle__label"
ng-click="toggleColorRangeSection()"
>
<span
aria-hidden="true"
ng-class="{ 'fa-caret-down': showColorRange, 'fa-caret-right': !showColorRange }"
class="fa fa-caret-right kuiSideBarCollapsibleTitle__caret"
></span>
<span class="kuiSideBarCollapsibleTitle__text">
Custom Ranges
</span>
</div>
<input aria-label="enable"
ng-model="vis.params.setColorRange"
type="checkbox"
class="kuiSideBarSectionTitle__action"
ng-click="toggleColorRangeSection(true)"
>
</div>
<div ng-if="vis.params.setColorRange" ng-show="showColorRange" class="kuiSideBarCollapsibleSection">
<div class="kuiSideBarSection">
<table class="vis-editor-agg-editor-ranges form-group" ng-show="vis.params.colorsRange.length">
<tr>
<th>
<label>From</label>
</th>
<th colspan="2">
<label>To</label>
</th>
</tr>
<tr ng-repeat="range in vis.params.colorsRange track by $index">
<td>
<input
ng-model="range.from"
type="number"
class="form-control"
name="range.from"
greater-or-equal-than="{{getGreaterThan($index)}}"
step="any" />
</td>
<td>
<input
ng-model="range.to"
type="number"
class="form-control"
name="range.to"
greater-than="range.from"
step="any" />
</td>
<td>
<button
type="button"
ng-click="removeRange($index)"
class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</td>
</tr>
</table>
<div class="hintbox" ng-show="!vis.params.colorsRange.length">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You must specify at least one range.
</p>
</div>
<div
ng-click="addRange()"
class="sidebar-item-button primary">
Add Range
</div>
<div class="text text-center text-info">Note: colors can be changed in the legend</div>
</div>
</div>
</div>
<div>
<div class="kuiSideBarCollapsibleTitle">
<div
class="kuiSideBarCollapsibleTitle__label"
ng-click="toggleLabelSection()"
>
<span
aria-hidden="true"
ng-class="{
'fa-caret-down': showLabels,
'fa-caret-right': !showLabels
}"
class="fa fa-caret-right kuiSideBarCollapsibleTitle__caret"
></span>
<span class="kuiSideBarCollapsibleTitle__text">
Show Labels
</span>
</div>
<input aria-label="enable"
ng-model="valueAxis.labels.show"
type="checkbox"
class="kuiSideBarSectionTitle__action"
>
</div>
<div ng-if="valueAxis.labels.show" ng-show="showLabels" class="kuiSideBarCollapsibleSection">
<div class="kuiSideBarSection">
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="rotateLabels">
Rotate
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="rotateLabels" type="checkbox" ng-model="options.rotateLabels">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="labelColor">
Color
</label>
<div class="kuiSideBarFormRow__control">
<input
id="labelColor"
class="kuiInput kuiSideBarInput"
ng-model="valueAxis.labels.color"
>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
import uiModules from 'ui/modules';
import heatmapOptionsTemplate from 'plugins/kbn_vislib_vis_types/controls/heatmap_options.html';
import _ from 'lodash';
const module = uiModules.get('kibana');
module.directive('heatmapOptions', function () {
return {
restrict: 'E',
template: heatmapOptionsTemplate,
replace: true,
link: function ($scope) {
$scope.showColorRange = false;
$scope.showLabels = false;
$scope.customColors = false;
$scope.options = {
rotateLabels: false
};
$scope.valueAxis = $scope.vis.params.valueAxes[0];
$scope.$watch('options.rotateLabels', rotate => {
$scope.vis.params.valueAxes[0].labels.rotate = rotate ? 270 : 0;
});
$scope.resetColors = function () {
$scope.uiState.set('vis.colors', null);
$scope.customColors = false;
};
$scope.toggleColorRangeSection = function (checkbox = false) {
$scope.showColorRange = !$scope.showColorRange;
if (checkbox && !$scope.vis.params.setColorRange) $scope.showColorRange = false;
if (!checkbox && $scope.showColorRange && !$scope.vis.params.setColorRange) $scope.vis.params.setColorRange = true;
};
$scope.toggleLabelSection = function (checkbox = false) {
$scope.showLabels = !$scope.showLabels;
if (checkbox && !$scope.valueAxis.labels.show) $scope.showLabels = false;
if ($scope.showLabels && !$scope.valueAxis.labels.show) $scope.valueAxis.labels.show = true;
};
$scope.getGreaterThan = function (index) {
if (index === 0) return;
return $scope.vis.params.colorsRange[index - 1].to;
};
$scope.addRange = function () {
const previousRange = _.last($scope.vis.params.colorsRange);
const from = previousRange ? previousRange.to : 0;
$scope.vis.params.colorsRange.push({ from: from, to: null });
};
$scope.removeRange = function (index) {
$scope.vis.params.colorsRange.splice(index, 1);
};
$scope.getColor = function (index) {
const defaultColors = this.uiState.get('vis.defaultColors');
const overwriteColors = this.uiState.get('vis.colors');
const colors = defaultColors ? _.defaults({}, overwriteColors, defaultColors) : overwriteColors;
return colors ? Object.values(colors)[index] : 'transparent';
};
$scope.uiState.on('colorChanged', () => {
$scope.customColors = true;
});
}
};
});

View file

@ -1,7 +1,11 @@
<div>
<label>
<input type="checkbox" value="{{smoothLines}}" ng-model="vis.params.smoothLines" name="smoothLines"
ng-checked="vis.params.smoothLines">
Smooth Lines
Line Mode
</label>
<select
class="form-control"
ng-model="vis.params.interpolate"
ng-options="mode.value as mode.text for mode in vis.type.params.interpolationModes"
>
</select>
</div>

View file

@ -0,0 +1,47 @@
<div class="kuiSideBarSection">
<div class="kuiSideBarSectionTitle">
<div class="kuiSideBarSectionTitle__text">
Basic Settings
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="addTooltip">
Show Tooltips
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="addTooltip" type="checkbox" ng-model="vis.params.addTooltip">
</div>
</div>
<div class="kuiSideBarFormRow">
<label class="kuiSideBarFormRow__label" for="enableHover">
Highlight
</label>
<div class="kuiSideBarFormRow__control">
<input class="kuiCheckBox" id="enableHover" type="checkbox" ng-model="vis.params.enableHover">
</div>
</div>
<div class="kuiSideBarFormRow" ng-show="vis.params.addLegend">
<label class="kuiSideBarFormRow__label" for="legendPosition">
Legend Position
</label>
<div class="kuiSideBarFormRow__control">
<select
id="legendPosition"
class="kuiSelect kuiSideBarSelect"
ng-model="vis.params.legendPosition"
ng-options="position.value as position.text for position in vis.type.params.legendPositions"
></select>
</div>
</div>
<div class="kuiSideBarSectionTitle">
<div class="kuiSideBarSectionTitle__text">
Heatmap Settings
</div>
</div>
<heatmap-options></heatmap-options>
</div>

View file

@ -0,0 +1,99 @@
import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type';
import VisSchemasProvider from 'ui/vis/schemas';
import heatmapTemplate from 'plugins/kbn_vislib_vis_types/editors/heatmap.html';
import heatmapColors from 'ui/vislib/components/color/colormaps';
export default function HeatmapVisType(Private) {
const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider);
const Schemas = Private(VisSchemasProvider);
return new VislibVisType({
name: 'heatmap',
title: 'Heatmap chart',
icon: 'fa-barcode',
description: 'A heat map is a graphical representation of data' +
' where the individual values contained in a matrix are represented as colors. ',
params: {
defaults: {
addTooltip: true,
addLegend: true,
enableHover: false,
legendPosition: 'right',
times: [],
colorsNumber: 4,
colorSchema: 'Greens',
setColorRange: false,
colorsRange: [],
invertColors: false,
percentageMode: false,
valueAxes: [{
show: false,
id: 'ValueAxis-1',
type: 'value',
scale: {
type: 'linear',
defaultYExtents: false,
},
labels: {
show: false,
rotate: 0,
color: '#555'
}
}]
},
legendPositions: [{
value: 'left',
text: 'left',
}, {
value: 'right',
text: 'right',
}, {
value: 'top',
text: 'top',
}, {
value: 'bottom',
text: 'bottom',
}],
scales: ['linear', 'log', 'square root'],
colorSchemas: Object.keys(heatmapColors),
editor: heatmapTemplate
},
schemas: new Schemas([
{
group: 'metrics',
name: 'metric',
title: 'Value',
min: 1,
max: 1,
aggFilter: ['count', 'avg', 'median', 'sum', 'min', 'max', 'cardinality', 'std_dev'],
defaults: [
{ schema: 'metric', type: 'count' }
]
},
{
group: 'buckets',
name: 'segment',
title: 'X-Axis',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
},
{
group: 'buckets',
name: 'group',
title: 'Y-Axis',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
},
{
group: 'buckets',
name: 'split',
title: 'Split Chart',
min: 0,
max: 1,
aggFilter: '!geohash_grid'
}
])
});
}

View file

@ -4,3 +4,4 @@ visTypes.register(require('plugins/kbn_vislib_vis_types/line'));
visTypes.register(require('plugins/kbn_vislib_vis_types/pie'));
visTypes.register(require('plugins/kbn_vislib_vis_types/area'));
visTypes.register(require('plugins/kbn_vislib_vis_types/tile_map'));
visTypes.register(require('plugins/kbn_vislib_vis_types/heatmap'));

View file

@ -18,7 +18,6 @@ export default function HistogramVisType(Private) {
addLegend: true,
legendPosition: 'right',
showCircles: true,
smoothLines: false,
interpolate: 'linear',
scale: 'linear',
drawLinesBetweenPoints: true,
@ -41,6 +40,16 @@ export default function HistogramVisType(Private) {
value: 'bottom',
text: 'bottom',
}],
interpolationModes: [{
value: 'linear',
text: 'straight',
}, {
value: 'cardinal',
text: 'smoothed',
}, {
value: 'step-after',
text: 'stepped',
}],
scales: ['linear', 'log', 'square root'],
editor: lineTemplate
},

View file

@ -1,17 +1,17 @@
import _ from 'lodash';
import supports from 'ui/utils/supports';
import VislibVisTypeVislibVisTypeProvider from 'ui/vislib_vis_type/vislib_vis_type';
import MapsVisTypeVislibVisTypeProvider from 'ui/vis_maps/maps_vis_type';
import VisSchemasProvider from 'ui/vis/schemas';
import AggResponseGeoJsonGeoJsonProvider from 'ui/agg_response/geo_json/geo_json';
import FilterBarPushFilterProvider from 'ui/filter_bar/push_filter';
import tileMapTemplate from 'plugins/kbn_vislib_vis_types/editors/tile_map.html';
export default function TileMapVisType(Private, getAppState, courier, config) {
const VislibVisType = Private(VislibVisTypeVislibVisTypeProvider);
const MapsVisType = Private(MapsVisTypeVislibVisTypeProvider);
const Schemas = Private(VisSchemasProvider);
const geoJsonConverter = Private(AggResponseGeoJsonGeoJsonProvider);
return new VislibVisType({
return new MapsVisType({
name: 'tile_map',
title: 'Tile map',
icon: 'fa-map-marker',

View file

@ -16,6 +16,6 @@ require('plugins/kibana/management/saved_object_registry').register({
});
// This is the only thing that gets injected into controllers
module.service('savedDashboards', function (SavedDashboard, kbnIndex, es, kbnUrl) {
return new SavedObjectLoader(SavedDashboard, kbnIndex, es, kbnUrl);
module.service('savedDashboards', function (SavedDashboard, kbnIndex, esAdmin, kbnUrl) {
return new SavedObjectLoader(SavedDashboard, kbnIndex, esAdmin, kbnUrl);
});

View file

@ -10,9 +10,6 @@
<ul class="list-unstyled sidebar-item index-pattern-selection">
<li css-truncate class="sidebar-item-title" title="{{id}}" ng-repeat="id in indexPatternList | orderBy" ng-show="indexPattern.id != id" ng-click="setIndexPattern(id)">{{id}}</li>
</ul>
<div ng-click="showIndexPatternSelection = !showIndexPatternSelection" class="discover-field-details-close">
<i class="fa fa-chevron-up"></i>
</div>
</div>
</div>
<div ng-hide="indexPatternList.length > 1">
@ -49,9 +46,6 @@
</div>
<div class="sidebar-item" ng-show="showFilter">
<div ng-click="showFilter = !showFilter" class="discover-field-details-close">
<i aria-hidden="true" class="fa fa-chevron-up"></i>
</div>
<form role="form" class="discover-field-details">
<div class="form-group">
<label>

View file

@ -4,7 +4,6 @@ import 'plugins/kibana/discover/saved_searches/_saved_search';
import 'ui/notify';
import uiModules from 'ui/modules';
import { SavedObjectLoader } from 'ui/courier/saved_object/saved_object_loader';
const module = uiModules.get('discover/saved_searches', [
'kibana/notify'
]);
@ -16,8 +15,8 @@ require('plugins/kibana/management/saved_object_registry').register({
title: 'searches'
});
module.service('savedSearches', function (Promise, config, kbnIndex, es, createNotifier, SavedSearch, kbnUrl) {
const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnIndex, es, kbnUrl);
module.service('savedSearches', function (Promise, config, kbnIndex, esAdmin, createNotifier, SavedSearch, kbnUrl) {
const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnIndex, esAdmin, kbnUrl);
// Customize loader properties since adding an 's' on type doesn't work for type 'search' .
savedSearchLoader.loaderProperties = {
name: 'searches',
@ -28,5 +27,6 @@ module.service('savedSearches', function (Promise, config, kbnIndex, es, createN
savedSearchLoader.urlFor = function (id) {
return kbnUrl.eval('#/discover/{{id}}', { id: id });
};
return savedSearchLoader;
});

View file

@ -161,18 +161,6 @@
color: @discover-field-details-color;
}
.discover-field-details-close {
text-align: center;
border-top: 1px solid;
border-color: @discover-field-details-close-border;
background-color: @discover-field-details-close-bg;
&:hover {
background-color: @discover-field-details-close-hover-bg;
color: @discover-field-details-close-hover-color;
}
}
.discover-field-details-count {
white-space: nowrap;
}

View file

@ -1,6 +1,6 @@
export default function RefreshKibanaIndexFn(es, kbnIndex) {
export default function RefreshKibanaIndexFn(esAdmin, kbnIndex) {
return function () {
return es.indices.refresh({
return esAdmin.indices.refresh({
index: kbnIndex
});
};

View file

@ -1,80 +1,182 @@
<kbn-management-app section="kibana">
<kbn-management-objects class="container-fluid">
<div class="header">
<h2 class="title">Edit Saved Objects</h2>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export Everything</button>
<file-upload on-read="importAll(fileContents)" upload-selector="button.upload">
<button class="btn btn-default controls upload" ng-click>
<i aria-hidden="true" class="fa fa-upload"></i> Import
<kbn-management-app section="kibana" class="kuiView">
<kbn-management-objects class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- Header -->
<div class="kuiViewContentItem kuiSubHeader">
<h1 class="kuiTitle">
Edit Saved Objects
</h1>
<div>
<button
class="kuiButton kuiButton--basic kuiButton--iconText"
ng-click="exportAll()"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-download"></span>
Export Everything
</button>
</file-upload>
<file-upload
on-read="importAll(fileContents)"
upload-selector="[data-import-saved-objects-button]"
>
<button
class="kuiButton kuiButton--basic kuiButton--iconText"
data-import-saved-objects-button
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-upload"></span>
Import
</button>
</file-upload>
</div>
</div>
<p>
<!-- Intro -->
<p class="kuiViewContentItem kuiVerticalRhythm">
From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen. Each tab is limited to 100 results. You can use the filter to find objects not in the default list.
</p>
<form role="form">
<input aria-label="Filter" ng-model="advancedFilter" class="form-control span12" type="text" placeholder="Filter"/>
</form>
<ul class="nav nav-tabs">
<li class="kbn-management-tab" ng-class="{ active: state.tab === service.title }" ng-repeat="service in services">
<a title="{{ service.title }}" ng-click="changeTab(service)">{{ service.title }}
<!-- Tabs -->
<div class="kuiViewContentItem kuiVerticalRhythm">
<div class="kuiTabs">
<button
class="kuiTab kbn-management-tab"
ng-class="{ 'kuiTab-isSelected': state.tab === service.title }"
ng-repeat="service in services"
title="{{ service.title }}"
ng-click="changeTab(service)"
>
{{ service.title }}
<small>
({{service.data.length}}<span ng-show="service.total > service.data.length"> of {{service.total}}</span>)
</small>
</a>
</li>
</ul>
<div class="tab-content">
<div class="action-bar">
<label>
<input type="checkbox" ng-checked="currentTab.data.length > 0 && selectedItems.length == currentTab.data.length" ng-click="toggleAll()" />
Select All
</label>
<a ng-disabled="selectedItems.length == 0"
confirm-click="bulkDelete()"
confirmation="Are you sure you want to delete the selected {{currentTab.title}}? This action is irreversible!"
class="btn btn-xs btn-danger" aria-label="Delete"><i aria-hidden="true" class="fa fa-trash"></i> Delete</a>
<a ng-disabled="selectedItems.length == 0"
ng-click="bulkExport()"
class="btn btn-xs btn-default" aria-label="Export"><i aria-hidden="true" class="fa fa-download"></i> Export</a>
</div>
<div ng-repeat="service in services" ng-class="{ active: state.tab === service.title }" class="tab-pane">
<ul class="list-unstyled">
<li class="item" ng-repeat="item in service.data | orderBy:'title'">
<div class="actions pull-right">
<button
ng-click="edit(service, item)"
class="btn btn-default"
aria-label="Edit">
<span class="sr-only">Edit</span>
<i aria-hidden="true" class="fa fa-pencil"></i>
</button>
<button
ng-click="open(item)"
class="btn btn-info"
aria-label="Hide">
<span class="sr-only">Hide</span>
<i aria-hidden="true" class="fa fa-eye"></i>
</button>
</div>
<div class="pull-left">
<input
ng-click="toggleItem(item)"
ng-checked="selectedItems.indexOf(item) >= 0"
type="checkbox" >
</div>
<div class="item-title">
<a ng-click="edit(service, item)">{{ item.title }}</a>
</div>
</li>
<li ng-if="!service.data.length" class="empty">No "{{service.title}}" found.</li>
</ul>
</button>
</div>
</div>
<!-- ControlledTable -->
<div
class="kuiViewContentItem kuiControlledTable kuiVerticalRhythm"
ng-repeat="service in services track by $index"
ng-show="state.tab === service.title"
>
<!-- ToolBar -->
<div class="kuiToolBar">
<div class="kuiToolBarSearch">
<div class="kuiToolBarSearchBox">
<div class="kuiToolBarSearchBox__icon kuiIcon fa-search"></div>
<input
class="kuiToolBarSearchBox__input"
type="text"
placeholder="Search..."
aria-label="Filter"
ng-model="managementObjectsController.advancedFilter"
>
</div>
</div>
<div class="kuiToolBarSection">
<!-- Bulk delete button -->
<button
class="kuiButton kuiButton--danger kuiButton--iconText"
confirm-click="bulkDelete()"
confirmation="Are you sure you want to delete the selected {{currentTab.title}}? This action is irreversible!"
aria-label="Delete selected objects"
ng-disabled="selectedItems.length == 0"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-trash"></span>
Delete
</button>
<!-- Bulk export button -->
<button
class="kuiButton kuiButton--basic kuiButton--iconText"
ng-click="bulkExport()"
aria-label="Export selected objects"
ng-disabled="selectedItems.length == 0"
>
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-download"></span>
Export
</button>
</div>
<div class="kuiToolBarSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
</div>
</div>
<!-- NoResults -->
<div class="kuiPanel kuiPanel--centered" ng-if="!service.data.length">
<div class="kuiNoItems">
No {{service.title}} matched your search.
</div>
</div>
<!-- Table -->
<table class="kuiTable" ng-if="service.data.length">
<thead>
<tr>
<th class="kuiTableHeaderCell kuiTableHeaderCell--checkBox">
<input
type="checkbox"
class="kuiCheckBox"
ng-checked="managementObjectsController.areAllRowsChecked()"
ng-click="toggleAll()"
>
</th>
<th class="kuiTableHeaderCell">
Title
</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="item in service.data | orderBy:'title'"
class="kuiTableRow"
>
<td class="kuiTableRowCell kuiTableRowCell--checkBox">
<input
type="checkbox"
class="kuiCheckBox"
ng-click="toggleItem(item)"
ng-checked="selectedItems.indexOf(item) >= 0"
>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<a class="kuiLink" href="" ng-click="edit(service, item)">
{{ item.title }}
</a>
<button
class="kuiMicroButton kuiTableRowHoverReveal"
ng-click="open(item)"
aria-label="View"
tooltip="View in app"
>
<span
aria-hidden="true"
class="kuiIcon fa-eye"
></span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- ToolBarFooter -->
<div class="kuiToolBarFooter">
<div class="kuiToolBarFooterSection">
<div class="kuiToolBarText" ng-hide="selectedItems.length === 0">
{{ selectedItems.length }} selected
</div>
</div>
<div class="kuiToolBarFooterSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
</div>
</div>
</div>
</kbn-management-objects>
</kbn-management-app>

View file

@ -18,13 +18,22 @@ uiModules.get('apps/management')
.directive('kbnManagementObjects', function (kbnIndex, Notifier, Private, kbnUrl, Promise) {
return {
restrict: 'E',
controller: function ($scope, $injector, $q, AppState, es) {
controllerAs: 'managementObjectsController',
controller: function ($scope, $injector, $q, AppState, esAdmin) {
const notify = new Notifier({ location: 'Saved Objects' });
// TODO: Migrate all scope variables to the controller.
const $state = $scope.state = new AppState();
$scope.currentTab = null;
$scope.selectedItems = [];
this.areAllRowsChecked = function areAllRowsChecked() {
if ($scope.currentTab.data.length === 0) {
return false;
}
return $scope.selectedItems.length === $scope.currentTab.data.length;
};
const getData = function (filter) {
const services = registry.all().map(function (obj) {
const service = $injector.get(obj.service);
@ -51,7 +60,11 @@ uiModules.get('apps/management')
});
};
const refreshData = () => {
return getData(this.advancedFilter);
};
// TODO: Migrate all scope methods to the controller.
$scope.toggleAll = function () {
if ($scope.selectedItems.length === $scope.currentTab.data.length) {
$scope.selectedItems.length = 0;
@ -60,6 +73,7 @@ uiModules.get('apps/management')
}
};
// TODO: Migrate all scope methods to the controller.
$scope.toggleItem = function (item) {
const i = $scope.selectedItems.indexOf(item);
if (i >= 0) {
@ -69,10 +83,12 @@ uiModules.get('apps/management')
}
};
// TODO: Migrate all scope methods to the controller.
$scope.open = function (item) {
kbnUrl.change(item.url.substr(1));
};
// TODO: Migrate all scope methods to the controller.
$scope.edit = function (service, item) {
const params = {
service: service.serviceName,
@ -82,6 +98,7 @@ uiModules.get('apps/management')
kbnUrl.change('/management/kibana/objects/{{ service }}/{{ id }}', params);
};
// TODO: Migrate all scope methods to the controller.
$scope.bulkDelete = function () {
$scope.currentTab.service.delete(pluck($scope.selectedItems, 'id'))
.then(refreshData)
@ -91,11 +108,13 @@ uiModules.get('apps/management')
.catch(error => notify.error(error));
};
// TODO: Migrate all scope methods to the controller.
$scope.bulkExport = function () {
const objs = $scope.selectedItems.map(partialRight(extend, { type: $scope.currentTab.type }));
retrieveAndExportDocs(objs);
};
// TODO: Migrate all scope methods to the controller.
$scope.exportAll = () => Promise
.map($scope.services, service => service.service
.scanAll('')
@ -106,7 +125,7 @@ uiModules.get('apps/management')
function retrieveAndExportDocs(objs) {
if (!objs.length) return notify.error('No saved objects to export.');
es.mget({
esAdmin.mget({
index: kbnIndex,
body: { docs: objs.map(transformToMget) }
})
@ -125,6 +144,7 @@ uiModules.get('apps/management')
saveAs(blob, 'export.json');
}
// TODO: Migrate all scope methods to the controller.
$scope.importAll = function (fileContents) {
let docs;
try {
@ -138,7 +158,7 @@ uiModules.get('apps/management')
return service.get().then(function (obj) {
obj.id = doc._id;
return obj.applyESResp(doc).then(function () {
return obj.save();
return obj.save({ confirmOverwrite : true });
});
});
})
@ -147,15 +167,12 @@ uiModules.get('apps/management')
};
function refreshIndex() {
return es.indices.refresh({
return esAdmin.indices.refresh({
index: kbnIndex
});
}
function refreshData() {
return getData($scope.advancedFilter);
}
// TODO: Migrate all scope methods to the controller.
$scope.changeTab = function (tab) {
$scope.currentTab = tab;
$scope.selectedItems.length = 0;
@ -163,7 +180,7 @@ uiModules.get('apps/management')
$state.save();
};
$scope.$watch('advancedFilter', function (filter) {
$scope.$watch('managementObjectsController.advancedFilter', function (filter) {
getData(filter);
});
}

View file

@ -1,36 +1,129 @@
<kbn-management-app section="kibana">
<kbn-management-objects-view class="container">
<div class="pull-right" style="margin-top: 20px;">
<a href="{{ link }}" class="btn btn-primary">View {{ title }}</a>
<a confirm-click="delete()" class="btn btn-danger"><i class="fa fa-trash-o"></i> Delete {{ title }} Object</a>
</div>
<h1>Edit {{ title }} Object</h1>
<div class="bs-callout bs-callout-danger" ng-if="notFound">
<h4>There is a problem with that saved object</h4>
<kbn-management-app section="kibana" class="kuiView">
<kbn-management-objects-view class="kuiViewContent kuiViewContent--constrainedWidth">
<!-- Header -->
<div class="kuiViewContentItem kuiSubHeader">
<h1 class="kuiTitle">
Edit {{ title }}
</h1>
<p ng-if="notFound === 'search'">The saved search associated with this object no longer exists.</p>
<p ng-if="notFound === 'index-pattern'">The index pattern associated with this object no longer exists.</p>
<p ng-if="notFound === 'index-pattern-field'">A field associated with this object no longer exists in the index pattern.</p>
<div class="kuiButtonGroup">
<a
class="kuiButton kuiButton--basic kuiButton--iconText"
href="{{ link }}"
>
<span class="kuiButton__icon kuiIcon fa-eye"></span>
View {{ title }}
</a>
<p>If you know what this error means, go ahead and fix it - otherwise click the delete button above.</p>
</div>
<div class="bs-callout bs-callout-warning">
<h4>Proceed with caution</h4>
<p>Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn't be.</p>
</div>
<form role="form" name="objectForm" ng-submit="submit()">
<div class="form-group" ng-repeat="field in fields">
<label>{{ field.name }}</label>
<textarea rows="1" msd-elastic=" " ng-if="field.type === 'text'" ng-model="field.value" class="form-control span12"/>
<input ng-if="field.type === 'number'" type="number" ng-model="field.value" class="form-control span12"/>
<div ng-if="field.type === 'json' || field.type === 'array'" ui-ace="{ onLoad: aceLoaded, mode: 'json' }" id="{{field.name}}" ng-model="field.value" class="form-control"></div>
<input ng-if="field.type === 'boolean'" type="checkbox" ng-model="field.value" ng-checked="field.value">
<button
class="kuiButton kuiButton--danger kuiButton--iconText"
confirm-click="delete()"
>
<span class="kuiButton__icon kuiIcon fa-trash-o"></span>
Delete {{ title }}
</button>
</div>
</div>
<!-- Errors -->
<div class="bs-callout bs-callout-danger" ng-if="notFound">
<h4>There is a problem with this saved object</h4>
<p ng-if="notFound === 'search'">
The saved search associated with this object no longer exists.
</p>
<p ng-if="notFound === 'index-pattern'">
The index pattern associated with this object no longer exists.
</p>
<p ng-if="notFound === 'index-pattern-field'">
A field associated with this object no longer exists in the index pattern.
</p>
<p>
If you know what this error means, go ahead and fix it &mdash; otherwise click the delete button above.
</p>
</div>
<!-- Intro -->
<div class="kuiViewContentItem kuiVerticalRhythm">
<div class="kuiInfoPanel kuiInfoPanel--warning">
<p>
<span class="kuiIcon kuiIcon--warning fa-bolt"></span>
<strong>Proceed with caution!</strong>
</p>
<p>Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn&rsquo;t be.</p>
</div>
</div>
<div class="kuiViewContentItem kuiVerticalRhythm">
<!-- Form -->
<form
role="form"
name="objectForm"
ng-submit="submit()"
>
<div class="kuiFormSection" ng-repeat="field in fields">
<label for="{{ field.name }}" class="kuiFormLabel">
{{ field.name }}
</label>
<input
id="{{ field.name }}"
ng-if="field.type === 'number'"
class="kuiTextInput"
type="number"
ng-model="field.value"
>
<textarea
id="{{ field.name }}"
ng-if="field.type === 'text'"
class="kuiTextArea"
rows="1"
msd-elastic=" "
ng-model="field.value"
></textarea>
<input
ng-if="field.type === 'boolean'"
class="kuiCheckBox"
type="checkbox"
ng-model="field.value"
ng-checked="field.value"
>
<div
ng-if="field.type === 'json' || field.type === 'array'"
ui-ace="{ onLoad: aceLoaded, mode: 'json' }"
id="{{field.name}}"
ng-model="field.value"
class="form-control"
></div>
</div>
</form>
<!-- Actions -->
<div class="kuiButtonGroup">
<button
class="kuiButton kuiButton--primary"
aria-label="Save {{ title }} Object"
ng-click="submit()"
ng-disabled="objectForm.$invalid || aceInvalidEditors.length !==0"
>
Save {{ title }} Object
</button>
<button
class="kuiButton kuiButton--basic"
aria-label="Cancel"
ng-click="cancel()"
>
Cancel
</button>
</div>
</form>
<div class="form-group">
<button aria-label="Cancel" class="btn btn-primary" ng-click="cancel()">Cancel</button>
<button aria-label="Save {{ title }} Object" class="btn btn-success" ng-click="submit()" ng-disabled="objectForm.$invalid || aceInvalidEditors.length !==0">Save {{ title }} Object</button>
</div>
</kbn-management-objects-view>
</kbn-management-app>

View file

@ -16,7 +16,7 @@ uiModules.get('apps/management')
.directive('kbnManagementObjectsView', function (kbnIndex, Notifier) {
return {
restrict: 'E',
controller: function ($scope, $injector, $routeParams, $location, $window, $rootScope, es, Private) {
controller: function ($scope, $injector, $routeParams, $location, $window, $rootScope, esAdmin, Private) {
const notify = new Notifier({ location: 'SavedObject view' });
const castMappingType = Private(IndexPatternsCastMappingTypeProvider);
const serviceObj = registry.get($routeParams.service);
@ -104,7 +104,7 @@ uiModules.get('apps/management')
$scope.title = service.type;
es.get({
esAdmin.get({
index: kbnIndex,
type: service.type,
id: $routeParams.id
@ -163,7 +163,7 @@ uiModules.get('apps/management')
* @returns {type} description
*/
$scope.delete = function () {
es.delete({
esAdmin.delete({
index: kbnIndex,
type: service.type,
id: $routeParams.id
@ -191,7 +191,7 @@ uiModules.get('apps/management')
_.set(source, field.name, value);
});
es.index({
esAdmin.index({
index: kbnIndex,
type: service.type,
id: $routeParams.id,
@ -204,7 +204,7 @@ uiModules.get('apps/management')
};
function redirectHandler(action) {
return es.indices.refresh({
return esAdmin.indices.refresh({
index: kbnIndex
})
.then(function (resp) {

View file

@ -12,26 +12,15 @@ kbn-management-objects-view {
display: block;
}
/**
* 1. Allow navbar to get taller on narrow screens.
*/
.management-navbar {
min-height: 70px; /* 1 */
}
.tab-account {
background-color: @kibanaGray6;
background-color: #FFFFFF;
}
.tab-management {
background-color: @kibanaGray6;
background-color: #FFFFFF;
}
.settings-nav {
text-transform: capitalize;
}
li.kbn-management-tab:first-letter {
.kbn-management-tab:first-letter {
text-transform: capitalize;
}
@ -49,6 +38,9 @@ kbn-management-landing {
.panel-body {
padding-bottom: 30px;
position: relative;
border-left: 1px solid #E4E4E3;
border-right: 1px solid #E4E4E3;
border-bottom: 1px solid #E4E4E3;
}
}
@ -154,9 +146,6 @@ kbn-management-advanced {
}
kbn-management-objects-view {
label {
font-family: @font-family-monospace;
}
.ace_editor { height: 300px; }
}

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import $ from 'jquery';
import VislibComponentsColorColorPaletteProvider from 'ui/vislib/components/color/color_palette';
import VislibComponentsColorColorPaletteProvider from 'ui/vis/components/color/color_palette';
import uiModules from 'ui/modules';
uiModules
.get('kibana')

View file

@ -88,7 +88,7 @@
<div class="vis-editor-config" ng-show="sidebar.section == 'options'">
<!-- vis options -->
<vis-editor-vis-options vis="vis" saved-vis="savedVis"></vis-editor-vis-options>
<vis-editor-vis-options vis="vis" saved-vis="savedVis" ui-state="uiState"></vis-editor-vis-options>
</div>

View file

@ -15,6 +15,7 @@ uiModules
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$bind('vis', 'editableVis');
$scope.$watch('vis.type', (visType) => {
if (visType) {
this.showData = visType.schemas.buckets || visType.schemas.metrics;

View file

@ -1,6 +1,3 @@
<div class="sidebar-item" ng-show="vis.type.params.editor">
<div class="sidebar-item-title">
view options
</div>
<div class="visualization-options"></div>
</div>

View file

@ -12,6 +12,7 @@ uiModules
scope: {
vis: '=',
savedVis: '=',
uiState: '=',
},
link: function ($scope, $el) {
const $optionContainer = $el.find('.visualization-options');

View file

@ -15,13 +15,13 @@ require('plugins/kibana/management/saved_object_registry').register({
title: 'visualizations'
});
app.service('savedVisualizations', function (Promise, es, kbnIndex, SavedVis, Private, Notifier, kbnUrl) {
app.service('savedVisualizations', function (Promise, esAdmin, kbnIndex, SavedVis, Private, Notifier, kbnUrl) {
const visTypes = Private(RegistryVisTypesProvider);
const notify = new Notifier({
location: 'Saved Visualization Service'
});
const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnIndex, es, kbnUrl);
const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnIndex, esAdmin, kbnUrl);
saveVisualizationLoader.mapHits = function (hit) {
const source = hit._source;
source.id = hit._id;

View file

@ -6,7 +6,7 @@ export function registerFieldCapabilities(server) {
path: '/api/kibana/{indices}/field_capabilities',
method: ['GET'],
handler: function (req, reply) {
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const indices = req.params.indices || '';
return callWithRequest(req, 'fieldStats', {

View file

@ -6,7 +6,7 @@ export function registerLanguages(server) {
path: '/api/kibana/scripts/languages',
method: 'GET',
handler: function (request, reply) {
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
return callWithRequest(request, 'cluster.getSettings', {
include_defaults: true,

View file

@ -6,7 +6,8 @@ export default function registerCount(server) {
path: '/api/kibana/{id}/_count',
method: ['POST', 'GET'],
handler: function (req, reply) {
const boundCallWithRequest = _.partial(server.plugins.elasticsearch.callWithRequest, req);
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const boundCallWithRequest = _.partial(callWithRequest, req);
boundCallWithRequest('count', {
allowNoIndices: false,

View file

@ -10,25 +10,25 @@
<i class="fa fa-danger"></i> Request Failed
</div>
<div ng-show="spy.mode.name === 'request'">
<div ng-if="spy.mode.name === 'request'">
<label>
Elasticsearch request body &nbsp;
</label>
<pre>{{req.fetchParams.body | json}}</pre>
</div>
<div ng-show="spy.mode.name === 'response'">
<div ng-if="spy.mode.name === 'response'">
<label>
Elasticsearch response body &nbsp;
</label>
<pre>{{req.resp | json}}</pre>
</div>
<div ng-show="spy.mode.name === 'stats'">
<div ng-if="spy.mode.name === 'stats'">
<table class="table">
<tr ng-repeat="pair in stats">
<td>{{pair[0]}}</td>
<td>{{pair[1]}}</td>
</tr>
</table>
</div>
</div>

View file

@ -1,6 +1,6 @@
import d3 from 'd3';
import d3TagCloud from 'd3-cloud';
import vislibComponentsSeedColorsProvider from 'ui/vislib/components/color/seed_colors';
import vislibComponentsSeedColorsProvider from 'ui/vis/components/color/seed_colors';
import { EventEmitter } from 'events';

View file

@ -15,8 +15,8 @@ define(function (require) {
});
// This is the only thing that gets injected into controllers
module.service('savedSheets', function (Promise, SavedSheet, kbnIndex, es, kbnUrl) {
const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnIndex, es, kbnUrl);
module.service('savedSheets', function (Promise, SavedSheet, kbnIndex, esAdmin, kbnUrl) {
const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnIndex, esAdmin, kbnUrl);
savedSheetLoader.urlFor = function (id) {
return kbnUrl.eval('#/{{id}}', { id: id });
};

View file

@ -3,9 +3,8 @@ module.exports = function (server) {
method: 'GET',
path: '/api/timelion/validate/es',
handler: function (request, reply) {
return server.uiSettings().getAll(request).then((uiSettings) => {
const callWithRequest = server.plugins.elasticsearch.callWithRequest;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const timefield = uiSettings['timelion:es.timefield'];

View file

@ -10,17 +10,22 @@ import esResponse from './fixtures/es_response';
import Promise from 'bluebird';
import _ from 'lodash';
import { expect } from 'chai';
import sinon from 'sinon';
import invoke from './helpers/invoke_series_fn.js';
function stubResponse(response) {
return {
server: { plugins:{
elasticsearch: {
callWithRequest: function () {
return Promise.resolve(response);
server: {
plugins:{
elasticsearch: {
getCluster: sinon.stub().withArgs('data').returns({
callWithRequest: function () {
return Promise.resolve(response);
}
})
}
}
} }
}
};
}

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