mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge branch 'master' of github.com:elastic/kibana into metrics
This commit is contained in:
commit
2fb42d0e5d
258 changed files with 8418 additions and 1502 deletions
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -121,3 +121,5 @@ include::visualize/tilemap.asciidoc[]
|
|||
include::visualize/vertbar.asciidoc[]
|
||||
|
||||
include::visualize/tagcloud.asciidoc[]
|
||||
|
||||
include::visualize/heatmap.asciidoc[]
|
||||
|
|
|
@ -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
|
||||
|
|
83
docs/visualize/heatmap.asciidoc
Normal file
83
docs/visualize/heatmap.asciidoc
Normal 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[]
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
|
||||
root: true
|
||||
extends: '../../../.eslintrc'
|
||||
extends: '../../../../.eslintrc'
|
||||
|
||||
rules:
|
||||
block-scoped-var: off
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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')
|
||||
]
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
36
src/core_plugins/console/public/.eslintrc
Normal file
36
src/core_plugins/console/public/.eslintrc
Normal 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
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}]);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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' +
|
||||
'}'
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
36
src/core_plugins/console/public/tests/src/utils_tests.js
Normal file
36
src/core_plugins/console/public/tests/src/utils_tests.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 || '');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.'));
|
||||
|
|
134
src/core_plugins/elasticsearch/lib/__tests__/cluster.js
Normal file
134
src/core_plugins/elasticsearch/lib/__tests__/cluster.js
Normal 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"');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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/*');
|
||||
});
|
||||
|
|
|
@ -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-*');
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
};
|
39
src/core_plugins/elasticsearch/lib/client_logger.js
Normal file
39
src/core_plugins/elasticsearch/lib/client_logger.js
Normal 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() {}
|
||||
};
|
||||
}
|
110
src/core_plugins/elasticsearch/lib/cluster.js
Normal file
110
src/core_plugins/elasticsearch/lib/cluster.js
Normal 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);
|
||||
}
|
19
src/core_plugins/elasticsearch/lib/create_admin_cluster.js
Normal file
19
src/core_plugins/elasticsearch/lib/create_admin_cluster.js
Normal 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'));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
25
src/core_plugins/elasticsearch/lib/create_clusters.js
Normal file
25
src/core_plugins/elasticsearch/lib/create_clusters.js
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
27
src/core_plugins/elasticsearch/lib/create_data_cluster.js
Normal file
27
src/core_plugins/elasticsearch/lib/create_data_cluster.js
Normal 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'));
|
||||
}
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
}
|
18
src/core_plugins/elasticsearch/lib/ensure_not_tribe.js
Normal file
18
src/core_plugins/elasticsearch/lib/ensure_not_tribe.js
Normal 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;
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
47
src/core_plugins/elasticsearch/lib/parse_config.js
Normal file
47
src/core_plugins/elasticsearch/lib/parse_config.js
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
99
src/core_plugins/kbn_vislib_vis_types/public/heatmap.js
Normal file
99
src/core_plugins/kbn_vislib_vis_types/public/heatmap.js
Normal 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'
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 — 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’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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -12,6 +12,7 @@ uiModules
|
|||
scope: {
|
||||
vis: '=',
|
||||
savedVis: '=',
|
||||
uiState: '=',
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
const $optionContainer = $el.find('.visualization-options');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
</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
|
||||
</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>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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'];
|
||||
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue