Merge branch 'master' into feature/ingest

This commit is contained in:
Matthew Bargar 2016-05-24 17:22:14 -04:00
commit 02f24b176b
101 changed files with 1138 additions and 761 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
.aws-config.json
.signing-config.json
.ackrc
.DS_Store
.node_binaries

View file

@ -1 +1 @@
4.3.2
4.4.4

View file

@ -89,4 +89,5 @@ module.exports = function (grunt) {
// load task definitions
grunt.task.loadTasks('tasks');
grunt.task.loadTasks('tasks/build');
grunt.task.loadTasks('tasks/rebuild');
};

View file

@ -41,9 +41,9 @@ Visit [Elastic.co](http://www.elastic.co/guide/en/kibana/current/index.html) for
For the daring, snapshot builds are available. These builds are created after each commit to the master branch, and therefore are not something you should run in production.
| platform | | |
| --- | --- | --- |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-darwin-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-darwin-x64.zip) |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x64.zip) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x86.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x86.zip) |
| Windows | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-windows.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-windows.zip) |
| platform | | | | |
| --- | --- | --- | --- | --- |
| OSX | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-darwin-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-darwin-x64.zip) | | |
| Linux x64 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x64.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x64.zip) | [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana_5.0.0-snapshot_amd64.deb)| [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0_snapshot-1.x86_64.rpm) |
| Linux x86 | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x86.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-linux-x86.zip) | [deb](https://download.elastic.co/kibana/kibana-snapshot/kibana_5.0.0-snapshot_i386.deb) | [rpm](https://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0_snapshot-1.i386.rpm) |
| Windows | [tar](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-windows.tar.gz) | [zip](http://download.elastic.co/kibana/kibana-snapshot/kibana-5.0.0-snapshot-windows.zip) | | |

View file

@ -6,7 +6,7 @@ key Kibana functionality. By the end of this tutorial, you will have:
* Loaded a sample data set into your Elasticsearch installation
* Defined at least one index pattern
* Use the <<discover, Discover>> functionality to explore your data
* Used the <<discover, Discover>> functionality to explore your data
* Set up some <<visualize,_visualizations_>> to graphically represent your data
* Assembled visualizations into a <<dashboard,Dashboard>>

View file

@ -44,3 +44,5 @@ error messages.
information and all requests.
`ops.interval`:: *Default: 10000* Set the interval in milliseconds to sample system and process performance metrics.
The minimum value is 100.
`status.allowAnonymous`:: *Default: false* If authentication is enabled, setting this to `true` allows
unauthenticated users to access the Kibana server status API and status page.

View file

@ -197,7 +197,7 @@
"supertest-as-promised": "2.0.2"
},
"engines": {
"node": "4.3.2",
"npm": "2.14.22"
"node": "4.4.4",
"npm": "2.15.1"
}
}

View file

@ -16,7 +16,9 @@ module.exports = function (kibana) {
main: 'plugins/console/console',
icon: 'plugins/console/logo.svg',
injectVars: function (server, options) {
return options;
const varsToInject = options;
varsToInject.elasticsearchUrl = server.config().get('elasticsearch.url');
return varsToInject;
}
}
];
@ -33,11 +35,11 @@ module.exports = function (kibana) {
return new kibana.Plugin({
id: 'console',
require: [ 'elasticsearch' ],
config: function (Joi) {
return Joi.object({
enabled: Joi.boolean().default(true),
defaultServerUrl: Joi.string().default('http://localhost:9200'),
proxyFilter: Joi.array().items(Joi.string()).single().default(['.*']),
ssl: Joi.object({
verify: Joi.boolean(),
@ -88,10 +90,7 @@ module.exports = function (kibana) {
const proxyRouteConfig = {
validate: {
query: Joi.object().keys({
uri: Joi.string().uri({
allowRelative: false,
shema: ['http:', 'https:'],
}),
uri: Joi.string()
}).unknown(true),
},
@ -111,15 +110,23 @@ module.exports = function (kibana) {
],
handler(req, reply) {
const { uri } = req.query;
let baseUri = server.config().get('elasticsearch.url');
let { uri:path } = req.query;
baseUri = baseUri.replace(/\/+$/, '');
path = path.replace(/^\/+/, '');
const uri = baseUri + '/' + path;
const requestHeadersWhitelist = server.config().get('elasticsearch.requestHeadersWhitelist');
const filterHeaders = server.plugins.elasticsearch.filterHeaders;
reply.proxy({
uri,
mapUri: function (request, done) {
done(null, uri, filterHeaders(request.headers, requestHeadersWhitelist))
},
xforward: true,
passThrough: true,
onResponse(err, res, request, reply, settings, ttl) {
if (err != null) {
reply("Error connecting to '" + request.query.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);
}

View file

@ -10,15 +10,10 @@ let utils = require('./utils');
let _ = require('lodash');
const chrome = require('ui/chrome');
const defaultServerUrl = chrome.getInjected('defaultServerUrl');
$(document.body).removeClass('fouc');
// set the value of the server and/or the input and clear the output
function resetToValues(server, content) {
if (server != null) {
es.setBaseUrl(server);
}
// set the value of the input and clear the output
function resetToValues(content) {
if (content != null) {
input.update(content);
}
@ -31,10 +26,10 @@ function loadSavedState() {
if (sourceLocation == "stored") {
if (previousSaveState) {
resetToValues(previousSaveState.server, previousSaveState.content);
resetToValues(previousSaveState.content);
}
else {
resetToValues(defaultServerUrl);
resetToValues();
input.autoIndent();
}
}
@ -44,17 +39,14 @@ function loadSavedState() {
loadFrom.headers = {Accept: "application/vnd.github.v3.raw"};
}
$.ajax(loadFrom).done(function (data) {
resetToValues(defaultServerUrl, data);
resetToValues(data);
input.moveToNextRequestEdge(true);
input.highlightCurrentRequestsAndUpdateActionBar();
input.updateActionsBar();
});
}
else if (previousSaveState) {
resetToValues(previousSaveState.server);
}
else {
resetToValues(defaultServerUrl);
resetToValues();
}
input.moveToNextRequestEdge(true);
}
@ -69,15 +61,12 @@ function setupAutosave() {
}
timer = setTimeout(saveCurrentState, saveDelay);
});
es.addServerChangeListener(saveCurrentState);
}
function saveCurrentState() {
try {
var content = input.getValue();
var server = es.getBaseUrl();
history.updateCurrentState(server, content);
history.updateCurrentState(content);
}
catch (e) {
console.log("Ignoring saving error: " + e);

View file

@ -32,13 +32,4 @@ module.controller('SenseController', function SenseController($scope, docTitle)
event.preventDefault();
input.focus();
};
this.serverUrl = es.getBaseUrl();
// read server url changes into scope
es.addServerChangeListener((server) => {
$scope.$evalAsync(() => {
this.serverUrl = server;
});
});
});

View file

@ -7,27 +7,3 @@
</span>
</div>
</kbn-top-nav>
<navbar ng-show="chrome.getVisible()" name="sense-serverInput">
<form
name="serverInput"
class="fill inline-form"
ng-submit="sense.sendSelected()"
role="form">
<input
type="text"
placeholder="http://servername:port"
aria-label="Server Name"
class="form-control"
ng-focus="navbar.updateServerUrlHistory()"
ng-blur="navbar.commitServerUrlFormModel()"
sense-uib-typeahead="url for url in navbar.serverUrlHistory"
ng-model="navbar.serverUrlFormModel"
typeahead-append-to-body="true"
typeahead-focus-first="false"
required>
</form>
</navbar>

View file

@ -14,7 +14,6 @@ require('ui/modules')
scope: {},
link($scope, $el, attrs, sense) {
$scope.sense = sense
$scope.navbar.link($scope)
},
controllerAs: 'navbar',
controller: class SenseNavbarController {
@ -49,21 +48,6 @@ require('ui/modules')
this.menu.open('welcome')
}
this.updateServerUrlHistory();
}
link($scope) {
$scope.$watch('sense.serverUrl', (url) => {
this.serverUrlFormModel = url
})
}
updateServerUrlHistory() {
this.serverUrlHistory = history.getHistoricalServers();
}
commitServerUrlFormModel() {
es.setBaseUrl(this.serverUrlFormModel);
}
}
};

View file

@ -1,13 +1,8 @@
let _ = require('lodash');
let $ = require('jquery');
let baseUrl;
let serverChangeListeners = [];
let esVersion = [];
module.exports.getBaseUrl = function () {
return baseUrl;
};
module.exports.getVersion = function () {
return esVersion;
};
@ -15,15 +10,7 @@ module.exports.getVersion = function () {
module.exports.send = function (method, path, data, server, disable_auth_alert) {
var wrappedDfd = $.Deferred();
server = server || exports.getBaseUrl();
path = exports.constructESUrl(server, path);
var uname_password_re = /^(https?:\/\/)?(?:(?:([^\/]*):)?([^\/]*?)@)?(.*)$/;
var url_parts = path.match(uname_password_re);
var uname = url_parts[2];
var password = url_parts[3];
path = url_parts[1] + url_parts[4];
console.log("Calling " + path + " (uname: " + uname + " pwd: " + password + ")");
console.log("Calling " + path);
if (data && method == "GET") {
method = "POST";
}
@ -37,8 +24,6 @@ module.exports.send = function (method, path, data, server, disable_auth_alert)
cache: false,
crossDomain: true,
type: method,
password: password,
username: uname,
dataType: "text", // disable automatic guessing
};
@ -56,61 +41,8 @@ module.exports.send = function (method, path, data, server, disable_auth_alert)
return wrappedDfd;
};
module.exports.constructESUrl = function (server, path) {
if (!path) {
path = server;
server = exports.getBaseUrl();
}
if (path.indexOf("://") >= 0) {
return path;
}
if (server.indexOf("://") < 0) {
server = (document.location.protocol || "http:") + "//" + server;
}
if (server.substr(-1) == "/") {
server = server.substr(0, server.length - 1);
}
if (path.charAt(0) === "/") {
path = path.substr(1);
}
return server + "/" + path;
};
module.exports.forceRefresh = function () {
exports.setBaseUrl(baseUrl, true)
};
module.exports.setBaseUrl = function (base, force) {
if (baseUrl !== base || force) {
var old = baseUrl;
baseUrl = base;
exports.send("GET", "/").done(function (data, status, xhr) {
if (xhr.status === 200) {
// parse for version
var value = xhr.responseText;
try {
value = JSON.parse(value);
if (value.version && value.version.number) {
esVersion = value.version.number.split(".");
}
}
catch (e) {
}
}
_.each(serverChangeListeners, function (cb) {
cb(base, old)
});
}).fail(function () {
esVersion = []; // unknown
_.each(serverChangeListeners, function (cb) {
cb(base, old)
});
});
}
};
module.exports.addServerChangeListener = function (cb) {
serverChangeListeners.push(cb);
module.exports.constructESUrl = function (baseUri, path) {
baseUri = baseUri.replace(/\/+$/, '');
path = path.replace(/^\/+/, '');
return baseUri + '/' + path;
};

View file

@ -3,8 +3,6 @@ const { uniq } = require('lodash');
const storage = require('./storage');
const chrome = require('ui/chrome');
const defaultServerUrl = chrome.getInjected('defaultServerUrl');
const history = module.exports = {
restoreFromHistory() {
// default method for history.restoreFromHistory
@ -26,11 +24,7 @@ const history = module.exports = {
.map(key => storage.get(key));
},
getHistoricalServers() {
return uniq(history.getHistory().map(req => req.server));
},
addToHistory(server, endpoint, method, data) {
addToHistory(endpoint, method, data) {
var keys = history.getHistoryKeys();
keys.splice(0, 500); // only maintain most recent X;
$.each(keys, function (i, k) {
@ -41,18 +35,16 @@ const history = module.exports = {
var k = "hist_elem_" + timestamp;
storage.set(k, {
time: timestamp,
server: server,
endpoint: endpoint,
method: method,
data: data
});
},
updateCurrentState(server, content) {
updateCurrentState(content) {
var timestamp = new Date().getTime();
storage.set("editor_state", {
time: timestamp,
server: server === defaultServerUrl ? undefined : server,
content: content
});
},
@ -60,8 +52,8 @@ const history = module.exports = {
getSavedEditorState() {
const saved = storage.get('editor_state');
if (!saved) return;
const { time, server = defaultServerUrl, content } = saved;
return { time, server, content };
const { time, content } = saved;
return { time, content };
},
clearHistory($el) {

View file

@ -158,7 +158,7 @@ function sendCurrentRequestToES() {
((xhr.status >= 200 && xhr.status < 300) || xhr.status == 404)
) {
// we have someone on the other side. Add to history
history.addToHistory(es.getBaseUrl(), es_path, es_method, es_data);
history.addToHistory(es_path, es_method, es_data);
let value = xhr.responseText;

View file

@ -238,27 +238,7 @@ function setActiveApi(api) {
ACTIVE_API = api;
}
es.addServerChangeListener(function () {
var version = es.getVersion() || [];
var api;
switch (version[0]) {
case '5':
api = 'es_5_0';
break;
case '2':
api = 'es_2_0';
break;
case '1':
default:
api = 'es_1_0';
}
if (api) {
setActiveApi(api);
}
});
setActiveApi('es_5_0');
module.exports.setActiveApi = setActiveApi;
module.exports.getGlobalAutocompleteComponents = getGlobalAutocompleteComponents;

View file

@ -279,8 +279,6 @@ function autocomplete_retriever() {
}, 60000);
}
es.addServerChangeListener(retrieveAutocompleteInfoFromServer);
module.exports = _.assign(mappingObj, {
getFields: getFields,
getIndices: getIndices,

View file

@ -6,6 +6,8 @@ let RowParser = require('./row_parser');
let InputMode = require('./mode/input');
let utils = require('../utils');
let es = require('../es');
import chrome from 'ui/chrome';
const smartResize = require('../smart_resize');
function isInt(x) {
@ -538,7 +540,8 @@ function SenseEditor($el) {
es_method = req.method,
es_data = req.data;
var url = es.constructESUrl(es.getBaseUrl() || "localhost:9200", es_path);
const elasticsearchBaseUrl = chrome.getInjected('elasticsearchUrl');
var url = es.constructESUrl(elasticsearchBaseUrl, es_path);
var ret = 'curl -X' + es_method + ' "' + url + '"';
if (es_data && es_data.length) {

View file

@ -61,7 +61,6 @@ function updateSettings({ fontSize, wrapMode, autocomplete}) {
setWrapMode(wrapMode);
setAutocomplete(autocomplete);
require('./input').focus();
es.forceRefresh();
return getCurrentSettings();
}

View file

@ -9,7 +9,6 @@ var {test, module, ok, fail, asyncTest, deepEqual, equal, start} = QUnit;
module("Editor", {
setup: function () {
es.setBaseUrl("http://localhost:9200");
input.$el.show();
input.autocomplete._test.removeChangeListener();

View file

@ -5,6 +5,7 @@ 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();
@ -80,6 +81,7 @@ module.exports = function (server) {
server.expose('createClient', createClient);
server.expose('callWithRequestFactory', _.partial(callWithRequest, server));
server.expose('callWithRequest', callWithRequest(server, noAuthClient));
server.expose('filterHeaders', filterHeaders);
server.expose('errors', elasticsearch.errors);
return client;

View file

@ -1,5 +1,6 @@
import ingest from './server/routes/api/ingest';
import search from './server/routes/api/search';
import settings from './server/routes/api/settings';
module.exports = function (kibana) {
return new kibana.Plugin({
@ -78,6 +79,7 @@ module.exports = function (kibana) {
init: function (server, options) {
ingest(server);
search(server);
settings(server);
}
});

View file

@ -1,4 +1,3 @@
import angular from 'angular';
import ngMock from 'ng_mock';
import _ from 'lodash';
@ -18,19 +17,20 @@ let config;
let hits;
let indexPattern;
let indexPatternList;
let shortDotsValue;
// Sets up the directive, take an element, and a list of properties to attach to the parent scope.
const init = function ($elem, props) {
ngMock.inject(function ($rootScope, $compile, $timeout, _config_) {
shortDotsValue = _config_.get('shortDots:enable');
config = _config_;
config.set('shortDots:enable', false);
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
// Required for test to run solo. Sigh
$timeout(function () {
$elem.scope().$digest();
}, 0);
$timeout(() => $elem.scope().$digest(), 0);
$scope = $elem.isolateScope();
});
@ -39,6 +39,7 @@ const init = function ($elem, props) {
const destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
config.set('shortDots:enable', shortDotsValue);
};
describe('discover field chooser directives', function () {
@ -80,9 +81,7 @@ describe('discover field chooser directives', function () {
$scope.$digest();
}));
afterEach(function () {
destroy();
});
afterEach(() => destroy());
const getSections = function (ctx) {
return {
@ -108,17 +107,26 @@ describe('discover field chooser directives', function () {
it('should have 2 popular fields, 1 unpopular field and no selected fields', function (done) {
const section = getSections($elem);
const popular = find('popular');
const unpopular = find('unpopular');
expect(section.selected.find('li').length).to.be(0);
expect(section.popular.text()).to.contain('ssl');
expect(section.popular.text()).to.contain('@timestamp');
expect(section.popular.text()).to.not.contain('ip\n');
expect(popular).to.contain('ssl');
expect(popular).to.contain('@timestamp');
expect(popular).to.not.contain('ip\n');
expect(section.unpopular.text()).to.contain('extension');
expect(section.unpopular.text()).to.contain('machine.os');
expect(section.unpopular.text()).to.not.contain('ssl');
expect(unpopular).to.contain('extension');
expect(unpopular).to.contain('machine.os');
expect(unpopular).to.not.contain('ssl');
done();
function find(popularity) {
return section[popularity]
.find('.discover-field-name')
.map((i, el) => $(el).text())
.toArray();
}
});

View file

@ -32,16 +32,15 @@ chrome
lastUrlStore: window.sessionStorage,
activeIndicatorColor: '#656a76'
})
.setRootController('kibana', function ($scope, $rootScope, courier, config) {
function setDefaultTimezone() {
moment.tz.setDefault(config.get('dateFormat:tz'));
}
.setRootController('kibana', function ($scope, courier, config) {
// wait for the application to finish loading
$scope.$on('application.load', function () {
courier.start();
});
$scope.$on('init:config', setDefaultTimezone);
$scope.$on('change:config.dateFormat:tz', setDefaultTimezone);
config.watch('dateFormat:tz', setDefaultTimezone, $scope);
function setDefaultTimezone(tz) {
moment.tz.setDefault(tz);
}
});

View file

@ -1,11 +1,10 @@
import _ from 'lodash';
import 'ui/elastic_textarea';
import ConfigDefaultsProvider from 'ui/config/defaults';
import uiModules from 'ui/modules';
import advancedRowTemplate from 'plugins/kibana/settings/sections/advanced/advanced_row.html';
uiModules.get('apps/settings')
.directive('advancedRow', function (config, Notifier, Private) {
.directive('advancedRow', function (config, Notifier) {
return {
restrict: 'A',
replace: true,
@ -15,7 +14,6 @@ uiModules.get('apps/settings')
configs: '='
},
link: function ($scope) {
const configDefaults = Private(ConfigDefaultsProvider);
const notify = new Notifier();
const keyCodes = {
ESC: 27
@ -28,10 +26,10 @@ uiModules.get('apps/settings')
const loading = function (conf, fn) {
conf.loading = true;
fn()
.finally(function () {
conf.loading = conf.editing = false;
})
.catch(notify.fatal);
.then(function () {
conf.loading = conf.editing = false;
})
.catch(notify.fatal);
};
$scope.maybeCancel = function ($event, conf) {
@ -50,7 +48,7 @@ uiModules.get('apps/settings')
$scope.save = function (conf) {
loading(conf, function () {
if (conf.unsavedValue === conf.defVal) {
return config.clear(conf.name);
return config.remove(conf.name);
}
return config.set(conf.name, conf.unsavedValue);
@ -63,7 +61,7 @@ uiModules.get('apps/settings')
$scope.clear = function (conf) {
return loading(conf, function () {
return config.clear(conf.name);
return config.remove(conf.name);
});
};

View file

@ -2,7 +2,6 @@ import _ from 'lodash';
import registry from 'ui/registry/settings_sections';
import toEditableConfig from 'plugins/kibana/settings/sections/advanced/lib/to_editable_config';
import 'plugins/kibana/settings/sections/advanced/advanced_row';
import ConfigDefaultsProvider from 'ui/config/defaults';
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import indexTemplate from 'plugins/kibana/settings/sections/advanced/index.html';
@ -17,39 +16,25 @@ uiModules.get('apps/settings')
return {
restrict: 'E',
link: function ($scope) {
const configDefaults = Private(ConfigDefaultsProvider);
const keyCodes = {
ESC: 27
};
function isTypeComplex(conf) {
return !(conf.json || conf.array || conf.bool || conf.normal);
}
function notDefaultConfig(configName) {
return !(configName in configDefaults);
}
function readConfigVals() {
const configVals = config._vals();
const customConfig = Object.keys(configVals)
.filter(notDefaultConfig)
.map(name => toEditableConfig(false, name, configVals[name]));
$scope.configs = _(configDefaults)
.map((def, name) => toEditableConfig(def, name, configVals[name]))
.reject('readonly')
.concat(customConfig)
.value();
}
// react to changes of the config values
const unhook = $rootScope.$on('change:config', readConfigVals);
$scope.$on('$destroy', unhook);
config.watchAll(changed, $scope);
// initial config setup
readConfigVals();
changed();
function changed(values) {
const all = config.getAll();
const editable = _(all)
.map((def, name) => toEditableConfig({
def,
name,
value: def.userValue,
isCustom: config.isCustom(name)
}))
.value();
const writable = _.reject(editable, 'readonly');
$scope.configs = writable;
}
}
};
});

View file

@ -82,5 +82,5 @@ describe('Settings', function () {
});
function invoke({ def = false, name = 'woah', value = 'forreal' } = {}) {
return toEditableConfig(def, name, value);
return toEditableConfig({ def, name, value, isCustom: def === false });
}

View file

@ -8,10 +8,10 @@ import getEditorType from './get_editor_type';
* @param {object} current value of setting
* @returns {object} the editable config object
*/
function toEditableConfig(def, name, value) {
const isCustom = !def;
if (isCustom) def = {};
function toEditableConfig({ def, name, value, isCustom }) {
if (!def) {
def = {};
}
const conf = {
name,
value,

View file

@ -58,7 +58,7 @@ uiModules.get('apps/settings')
$scope.removePattern = function () {
if ($scope.indexPattern.id === config.get('defaultIndex')) {
config.delete('defaultIndex');
config.remove('defaultIndex');
if (otherIds.length) {
config.set('defaultIndex', otherIds[0]);
}

View file

@ -16,7 +16,7 @@ uiModules
$scope.delete = attrs.delete ? $scope.delete : null;
$scope.setDefault = attrs.setDefault ? $scope.setDefault : null;
$scope.refreshFields = attrs.refreshFields ? $scope.refreshFields : null;
config.$bind($scope, 'defaultIndex');
config.bindToScope($scope, 'defaultIndex');
}
};
});

View file

@ -14,7 +14,7 @@ uiModules.get('apps/settings')
$scope.showAddNew = !/^\/settings\/indices$/.test($route.current.$$route.originalPath);
$scope.editingId = $route.current.params.indexPatternId;
config.$bind($scope, 'defaultIndex');
config.bindToScope($scope, 'defaultIndex');
function refreshIndexPatternList() {
indexPatterns.getIds.clearCache();

View file

@ -241,6 +241,8 @@ uiModules
$scope.doSave = function () {
savedVis.id = savedVis.title;
// vis.title was not bound and it's needed to reflect title into visState
$state.vis.title = savedVis.title;
savedVis.visState = $state.vis;
savedVis.uiStateJSON = angular.toJson($scope.uiState.getChanges());

View file

@ -0,0 +1,6 @@
export default function (server) {
require('./register_get')(server);
require('./register_set')(server);
require('./register_set_many')(server);
require('./register_delete')(server);
}

View file

@ -0,0 +1,19 @@
import Boom from 'boom';
export default function registerDelete(server) {
server.route({
path: '/api/kibana/settings/{key}',
method: 'DELETE',
handler: function (req, reply) {
const { key } = req.params;
const uiSettings = server.uiSettings();
uiSettings
.remove(key)
.then(() => uiSettings
.getUserProvided()
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
}
});
}

View file

@ -0,0 +1,15 @@
import Boom from 'boom';
export default function registerGet(server) {
server.route({
path: '/api/kibana/settings',
method: 'GET',
handler: function (req, reply) {
server
.uiSettings()
.getUserProvided()
.then(settings => reply({ settings }).type('application/json'))
.catch(reason => reply(Boom.wrap(reason)));
}
});
}

View file

@ -0,0 +1,20 @@
import Boom from 'boom';
export default function registerSet(server) {
server.route({
path: '/api/kibana/settings/{key}',
method: 'POST',
handler: function (req, reply) {
const { key } = req.params;
const { value } = req.payload;
const uiSettings = server.uiSettings();
uiSettings
.set(key, value)
.then(() => uiSettings
.getUserProvided()
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
}
});
}

View file

@ -0,0 +1,20 @@
import Boom from 'boom';
export default function registerSet(server) {
server.route({
path: '/api/kibana/settings',
method: 'POST',
handler: function (req, reply) {
const { key } = req.params;
const { changes } = req.payload;
const uiSettings = server.uiSettings();
uiSettings
.setMany(changes)
.then(() => uiSettings
.getUserProvided()
.then(settings => reply({ settings }).type('application/json'))
)
.catch(reason => reply(Boom.wrap(reason)));
}
});
}

View file

@ -1,5 +1,5 @@
import { union } from 'lodash';
import defaultsProvider from '../../ui/settings/defaults';
import findSourceFiles from './find_source_files';
import { fromRoot } from '../../utils';
@ -57,6 +57,8 @@ export default (kibana) => {
});
}
env.defaultUiSettings = defaultsProvider();
return new UiBundle({
id: 'tests',
modules: modules,

View file

@ -1,12 +1,12 @@
module.exports = function ({env, bundle}) {
let pluginSlug = env.pluginInfo.sort()
.map(p => ' * - ' + p)
.join('\n');
const pluginSlug = env.pluginInfo.sort()
.map(p => ' * - ' + p)
.join('\n');
let requires = bundle.modules
.map(m => `require(${JSON.stringify(m)});`)
.join('\n');
const requires = bundle.modules
.map(m => `require(${JSON.stringify(m)});`)
.join('\n');
return `
/**
@ -28,13 +28,16 @@ window.__KBN__ = {
esShardTimeout: 1500,
esApiVersion: '2.0',
esRequestTimeout: '300000'
},
uiSettings: {
defaults: ${JSON.stringify(env.defaultUiSettings, null, 2).split('\n').join('\n ')},
user: {}
}
};
require('ui/test_harness');
${requires}
require('ui/test_harness').bootstrap(/* go! */);
`;
};

View file

@ -121,6 +121,10 @@ module.exports = () => Joi.object({
)
.default(Joi.ref('$dev')),
profile: Joi.boolean().default(false)
}).default(),
status: Joi.object({
allowAnonymous: Joi.boolean().default(false)
}).default()
}).default();

View file

@ -32,6 +32,9 @@ module.exports = class KbnServer {
// setup this.uiExports and this.bundles
require('../ui'),
// setup server.uiSettings
require('../ui/settings'),
// ensure that all bundles are built, or that the
// lazy bundle server is running
require('../optimize'),

View file

@ -0,0 +1,42 @@
import expect from 'expect.js';
import wrapAuthConfig from '../wrap_auth_config';
describe('Status wrapAuthConfig', () => {
let options;
beforeEach(() => {
options = {
method: 'GET',
path: '/status',
handler: function (request, reply) {
return reply();
}
};
});
it('should return a function', () => {
expect(wrapAuthConfig()).to.be.a('function');
expect(wrapAuthConfig(true)).to.be.a('function');
expect(wrapAuthConfig(false)).to.be.a('function');
});
it('should not add auth config by default', () => {
const wrapAuth = wrapAuthConfig();
const wrapped = wrapAuth(options);
expect(wrapped).to.not.have.property('config');
});
it('should not add auth config if allowAnonymous is false', () => {
const wrapAuth = wrapAuthConfig(false);
const wrapped = wrapAuth(options);
expect(wrapped).to.not.have.property('config');
});
it('should add auth config if allowAnonymous is true', () => {
const wrapAuth = wrapAuthConfig(true);
const wrapped = wrapAuth(options);
expect(wrapped).to.have.property('config');
expect(wrapped.config).to.have.property('auth');
expect(wrapped.config.auth).to.be(false);
});
});

View file

@ -1,15 +1,18 @@
import _ from 'lodash';
import ServerStatus from './server_status';
import wrapAuthConfig from './wrap_auth_config';
import { join } from 'path';
module.exports = function (kbnServer, server, config) {
export default function (kbnServer, server, config) {
kbnServer.status = new ServerStatus(kbnServer.server);
if (server.plugins.good) {
kbnServer.mixin(require('./metrics'));
}
server.route({
const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous'));
server.route(wrapAuth({
method: 'GET',
path: '/api/status',
handler: function (request, reply) {
@ -20,20 +23,27 @@ module.exports = function (kbnServer, server, config) {
metrics: kbnServer.metrics
});
}
}));
server.decorate('reply', 'renderStatusPage', async function () {
const app = kbnServer.uiExports.getHiddenApp('status_page');
const response = await getResponse(this);
response.code(kbnServer.status.isGreen() ? 200 : 503);
return response;
function getResponse(ctx) {
if (app) {
return ctx.renderApp(app);
}
return ctx(kbnServer.status.toString());
}
});
server.decorate('reply', 'renderStatusPage', function () {
let app = kbnServer.uiExports.getHiddenApp('status_page');
let resp = app ? this.renderApp(app) : this(kbnServer.status.toString());
resp.code(kbnServer.status.isGreen() ? 200 : 503);
return resp;
});
server.route({
server.route(wrapAuth({
method: 'GET',
path: '/status',
handler: function (request, reply) {
return reply.renderStatusPage();
}
});
}));
};

View file

@ -0,0 +1,6 @@
import {assign, identity} from 'lodash';
export default (allowAnonymous) => {
if (allowAnonymous) return options => assign(options, {config: {auth: false}});
return identity;
};

View file

@ -8,7 +8,8 @@ import UiExports from './ui_exports';
import UiBundle from './ui_bundle';
import UiBundleCollection from './ui_bundle_collection';
import UiBundlerEnv from './ui_bundler_env';
module.exports = async (kbnServer, server, config) => {
export default async (kbnServer, server, config) => {
const loadingGif = readFile(fromRoot('src/ui/public/loading.gif'), { encoding: 'base64'});
@ -59,7 +60,8 @@ module.exports = async (kbnServer, server, config) => {
}
});
server.decorate('reply', 'renderApp', function (app) {
server.decorate('reply', 'renderApp', async function (app) {
const uiSettings = server.uiSettings();
const payload = {
app: app,
nav: uiExports.navLinks.inOrder,
@ -68,6 +70,10 @@ module.exports = async (kbnServer, server, config) => {
buildSha: config.get('pkg.buildSha'),
basePath: config.get('server.basePath'),
serverName: config.get('server.name'),
uiSettings: {
defaults: await uiSettings.getDefaults(),
user: await uiSettings.getUserProvided()
},
vars: defaults(app.getInjectedVars() || {}, uiExports.defaultInjectedVars),
};

View file

@ -72,13 +72,13 @@ describe('AggConfig Filters', function () {
expect(fieldParams).to.have.property('gte');
expect(fieldParams.gte).to.be.a('number');
expect(fieldParams).to.have.property('lte');
expect(fieldParams.lte).to.be.a('number');
expect(fieldParams).to.have.property('lt');
expect(fieldParams.lt).to.be.a('number');
expect(fieldParams).to.have.property('format');
expect(fieldParams.format).to.be('epoch_millis');
expect(fieldParams.gte).to.be.lessThan(fieldParams.lte);
expect(fieldParams.gte).to.be.lessThan(fieldParams.lt);
expect(filter).to.have.property('meta');
expect(filter.meta).to.have.property('index', vis.indexPattern.id);
@ -102,7 +102,7 @@ describe('AggConfig Filters', function () {
let params = filter.range[field.name];
expect(params.gte).to.be(+bucketStart);
expect(params.lte).to.be(+bucketStart.clone().add(interval).subtract(1, 'ms'));
expect(params.lt).to.be(+bucketStart.clone().add(interval));
});
});
});

View file

@ -8,7 +8,7 @@ export default function createDateHistogramFilterProvider(Private) {
return buildRangeFilter(agg.params.field, {
gte: start.valueOf(),
lte: start.add(interval).subtract(1, 'ms').valueOf(),
lt: start.add(interval).valueOf(),
format: 'epoch_millis'
}, agg.vis.indexPattern);
};

View file

@ -1,6 +1,6 @@
import buildRangeFilter from 'ui/filter_manager/lib/range';
export default function createHistogramFitlerProvider(Private) {
export default function createHistogramFilterProvider(Private) {
return function (aggConfig, key) {
let value = parseInt(key, 10);

View file

@ -7,21 +7,21 @@ import AggTypesBucketsBucketAggTypeProvider from 'ui/agg_types/buckets/_bucket_a
import TimeBucketsProvider from 'ui/time_buckets';
import AggTypesBucketsCreateFilterDateHistogramProvider from 'ui/agg_types/buckets/create_filter/date_histogram';
import AggTypesBucketsIntervalOptionsProvider from 'ui/agg_types/buckets/_interval_options';
import ConfigDefaultsProvider from 'ui/config/defaults';
import intervalTemplate from 'ui/agg_types/controls/interval.html';
export default function DateHistogramAggType(timefilter, config, Private) {
let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
let TimeBuckets = Private(TimeBucketsProvider);
let createFilter = Private(AggTypesBucketsCreateFilterDateHistogramProvider);
let intervalOptions = Private(AggTypesBucketsIntervalOptionsProvider);
let configDefaults = Private(ConfigDefaultsProvider);
const BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
const TimeBuckets = Private(TimeBucketsProvider);
const createFilter = Private(AggTypesBucketsCreateFilterDateHistogramProvider);
const intervalOptions = Private(AggTypesBucketsIntervalOptionsProvider);
let detectedTimezone = tzDetect.determine().name();
let tzOffset = moment().format('Z');
const detectedTimezone = tzDetect.determine().name();
const tzOffset = moment().format('Z');
function getInterval(agg) {
let interval = _.get(agg, ['params', 'interval']);
if (interval && interval.val === 'custom') interval = _.get(agg, ['params', 'customInterval']);
const interval = _.get(agg, ['params', 'interval']);
if (interval && interval.val === 'custom') {
return _.get(agg, ['params', 'customInterval']);
}
return interval;
}
@ -39,8 +39,8 @@ export default function DateHistogramAggType(timefilter, config, Private) {
date: true
},
makeLabel: function (agg) {
let output = this.params.write(agg);
let params = output.params;
const output = this.params.write(agg);
const params = output.params;
return params.field + ' per ' + (output.metricScaleText || output.bucketInterval.description);
},
createFilter: createFilter,
@ -81,7 +81,7 @@ export default function DateHistogramAggType(timefilter, config, Private) {
name: 'interval',
type: 'optioned',
deserialize: function (state) {
let interval = _.find(intervalOptions, {val: state});
const interval = _.find(intervalOptions, {val: state});
return interval || _.find(intervalOptions, function (option) {
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
// but this maps the old values to the new values
@ -98,25 +98,26 @@ export default function DateHistogramAggType(timefilter, config, Private) {
setBounds(agg);
agg.buckets.setInterval(getInterval(agg));
let interval = agg.buckets.getInterval();
const interval = agg.buckets.getInterval();
output.bucketInterval = interval;
output.params.interval = interval.expression;
let isDefaultTimezone = config.get('dateFormat:tz') === configDefaults['dateFormat:tz'].value;
output.params.time_zone = isDefaultTimezone ?
(detectedTimezone || tzOffset) :
config.get('dateFormat:tz');
let scaleMetrics = interval.scaled && interval.scale < 1;
if (scaleMetrics) {
scaleMetrics = _.every(agg.vis.aggs.bySchemaGroup.metrics, function (agg) {
return agg.type && (agg.type.name === 'count' || agg.type.name === 'sum');
});
const isDefaultTimezone = config.isDefault('dateFormat:tz');
if (isDefaultTimezone) {
output.params.time_zone = detectedTimezone || tzOffset;
} else {
output.params.time_zone = config.get('dateFormat:tz');
}
const scaleMetrics = interval.scaled && interval.scale < 1;
if (scaleMetrics) {
output.metricScale = interval.scale;
output.metricScaleText = interval.preScaled.description;
const all = _.every(agg.vis.aggs.bySchemaGroup.metrics, function (agg) {
return agg.type && (agg.type.name === 'count' || agg.type.name === 'sum');
});
if (all) {
output.metricScale = interval.scale;
output.metricScaleText = interval.preScaled.description;
}
}
}
},
@ -140,7 +141,7 @@ export default function DateHistogramAggType(timefilter, config, Private) {
name: 'extended_bounds',
default: {},
write: function (agg, output) {
let val = agg.params.extended_bounds;
const val = agg.params.extended_bounds;
if (val.min != null || val.max != null) {
output.params.extended_bounds = {

View file

@ -1,5 +1,5 @@
import _ from 'lodash';
export default function BoundToConfigObjProvider($rootScope, config) {
export default function BoundToConfigObjProvider(config) {
/**
* Create an object with properties that may be bound to config values.
@ -18,21 +18,17 @@ export default function BoundToConfigObjProvider($rootScope, config) {
function BoundToConfigObj(input) {
const self = this;
_.forOwn(input, function (val, prop) {
if (!_.isString(val) || val.charAt(0) !== '=') {
self[prop] = val;
_.forOwn(input, function (value, prop) {
if (!_.isString(value) || value.charAt(0) !== '=') {
self[prop] = value;
return;
}
const configKey = val.substr(1);
update();
$rootScope.$on('init:config', update);
$rootScope.$on('change:config.' + configKey, update);
function update() {
self[prop] = config.get(configKey);
}
const configKey = value.substr(1);
config.watch(configKey, function update(value) {
self[prop] = value;
});
});
}

View file

@ -24,6 +24,7 @@ module.exports = function (chrome, internals) {
.value('buildNum', internals.buildNum)
.value('buildSha', internals.buildSha)
.value('serverName', internals.serverName)
.value('uiSettings', internals.uiSettings)
.value('sessionId', Date.now())
.value('chrome', chrome)
.value('esUrl', (function () {

View file

@ -1,16 +1,14 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import ConfigDefaultsProvider from 'ui/config/defaults';
describe('config component', function () {
let $scope;
let config;
let defaults;
let $scope;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private) {
config = $injector.get('config');
$scope = $injector.get('$rootScope');
defaults = Private(ConfigDefaultsProvider);
}));
describe('#get', function () {
@ -18,22 +16,15 @@ describe('config component', function () {
it('gives access to config values', function () {
expect(config.get('dateFormat')).to.be.a('string');
});
it('reads from the defaults', function () {
let initial = config.get('dateFormat');
let newDefault = initial + '- new';
defaults.dateFormat.value = newDefault;
expect(config.get('dateFormat')).to.be(newDefault);
});
});
describe('#set', function () {
it('stores a value in the config val set', function () {
let initial = config.get('dateFormat');
const original = config.get('dateFormat');
config.set('dateFormat', 'notaformat');
expect(config.get('dateFormat')).to.be('notaformat');
config.set('dateFormat', original);
});
});
@ -41,26 +32,27 @@ describe('config component', function () {
describe('#$bind', function () {
it('binds a config key to a $scope property', function () {
let dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat');
const dateFormat = config.get('dateFormat');
config.bindToScope($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
});
it('alows overriding the property name', function () {
let dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat', 'defaultDateFormat');
const dateFormat = config.get('dateFormat');
config.bindToScope($scope, 'dateFormat', 'defaultDateFormat');
expect($scope).to.not.have.property('dateFormat');
expect($scope).to.have.property('defaultDateFormat', dateFormat);
});
it('keeps the property up to date', function () {
let dateFormat = config.get('dateFormat');
let newDateFormat = dateFormat + ' NEW NEW NEW!';
config.$bind($scope, 'dateFormat');
const original = config.get('dateFormat');
const newDateFormat = original + ' NEW NEW NEW!';
config.bindToScope($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
expect($scope).to.have.property('dateFormat', original);
config.set('dateFormat', newDateFormat);
expect($scope).to.have.property('dateFormat', newDateFormat);
config.set('dateFormat', original);
});

View file

@ -1,87 +1,68 @@
import _ from 'lodash';
import angular from 'angular';
import ConfigValsProvider from 'ui/config/_vals';
import Notifier from 'ui/notify/notifier';
export default function DelayedUpdaterFactory($http, chrome, Promise) {
let unsavedChanges = {};
let unresolvedPromises = [];
let saveTimeout = null;
export default function DelayedUpdaterFactory(Private, $rootScope, Promise) {
let notify = new Notifier();
return function delayedUpdate(key, value) {
unsavedChanges[key] = value;
let vals = Private(ConfigValsProvider);
return function DelayedUpdater(doc) {
let updater = this;
let queue = [];
let log = {};
let timer;
updater.fire = function () {
clearTimeout(timer);
// only fire once
if (updater.fired) return;
updater.fired = true;
let method;
let body;
let updated = [];
let deleted = [];
// seperate the log into lists
Object.keys(log).forEach(function (key) {
if (log[key] === 'updated') updated.push(key);
else deleted.push(key);
});
if (deleted.length) {
method = 'doIndex';
body = _.clone(vals);
} else {
method = 'doUpdate';
body = _.pick(vals, updated);
}
doc[method](vals)
.then(
function (resp) {
queue.forEach(function (q) { q.resolve(resp); });
},
function (err) {
queue.forEach(function (q) { q.reject(err); });
}
)
.finally(function () {
$rootScope.$broadcast('change:config', updated.concat(deleted));
});
};
updater.update = function (key, val, silentAndLocal) {
let newVal = val;
let oldVal = vals[key];
if (angular.equals(newVal, oldVal)) {
return Promise.resolve();
}
else if (newVal == null) {
delete vals[key];
log[key] = 'deleted';
}
else {
vals[key] = newVal;
log[key] = 'updated';
}
if (silentAndLocal) return Promise.resolve();
let defer = Promise.defer();
queue.push(defer);
notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal);
$rootScope.$broadcast('change:config.' + key, newVal, oldVal);
// reset the fire timer
clearTimeout(timer);
timer = setTimeout(updater.fire, 200);
return defer.promise;
};
return new Promise(saveSoon)
.then(res => res.data.settings);
};
function saveSoon(resolve, reject) {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(fire, 200);
unresolvedPromises.push({ resolve, reject });
}
function fire() {
const changes = unsavedChanges;
const promises = unresolvedPromises;
unresolvedPromises = [];
unsavedChanges = {};
persist(changes)
.then(result => settle(promises, `resolve`, result))
.catch(reason => settle(promises, `reject`, reason));
}
function settle(listeners, decision, data) {
listeners.forEach(listener => listener[decision](data));
}
function persist(changes) {
const keys = Object.keys(changes);
if (keys.length === 1) {
const [key] = keys;
const value = changes[key];
const update = value === null ? remove : edit;
return update(key, value);
}
return editMany(changes);
}
function remove(key) {
return sync(`delete`, { postfix: `/${key}` });
}
function edit(key, value) {
return sync(`post`, { postfix: `/${key}`, data: { value } });
}
function editMany(changes) {
return sync(`post`, { data: { changes } });
}
function sync(method, { postfix = '', data } = {}) {
return $http({
method,
url: chrome.addBasePath(`/api/kibana/settings${postfix}`),
data
});
}
};

View file

@ -1,3 +0,0 @@
export default function ConfigValsService() {
return {};
};

View file

@ -1,124 +1,27 @@
import angular from 'angular';
import _ from 'lodash';
import ConfigDefaultsProvider from 'ui/config/defaults';
import ConfigDelayedUpdaterProvider from 'ui/config/_delayed_updater';
import ConfigValsProvider from 'ui/config/_vals';
import DocSourceProvider from 'ui/courier/data_source/doc_source';
import { once, cloneDeep, defaultsDeep, isPlainObject } from 'lodash';
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import Notifier from 'ui/notify/notifier';
let module = uiModules.get('kibana/config');
uiRoutes.addSetupWork(function (config) {
return config.init();
});
import ConfigDelayedUpdaterProvider from 'ui/config/_delayed_updater';
const module = uiModules.get('kibana/config');
// service for delivering config variables to everywhere else
module.service('config', function (Private, kbnVersion, kbnIndex, $rootScope, buildNum) {
let config = this;
module.service(`config`, function (Private, $rootScope, $http, chrome, uiSettings) {
const config = this;
const notify = new Notifier({ location: `Config` });
const { defaults, user: initialUserSettings } = uiSettings;
const delayedUpdate = Private(ConfigDelayedUpdaterProvider);
let settings = mergeSettings(defaults, initialUserSettings);
let defaults = Private(ConfigDefaultsProvider);
let DelayedUpdater = Private(ConfigDelayedUpdaterProvider);
let vals = Private(ConfigValsProvider);
let notify = new Notifier({
location: 'Config'
});
// active or previous instance of DelayedUpdater. This will log and then process an
// update once it is requested by calling #set() or #clear().
let updater;
let DocSource = Private(DocSourceProvider);
let doc = (new DocSource())
.index(kbnIndex)
.type('config')
.id(kbnVersion);
/******
* PUBLIC API
******/
/**
* Executes once and returns a promise that is resolved once the
* config has loaded for the first time.
*
* @return {Promise} - Resolved when the config loads initially
*/
config.init = _.once(function () {
let complete = notify.lifecycle('config init');
return (function getDoc() {
// used to apply an entire es response to the vals, silentAndLocal will prevent
// event/notifications/writes from occuring.
let applyMassUpdate = function (resp, silentAndLocal) {
_.union(_.keys(resp._source), _.keys(vals)).forEach(function (key) {
change(key, resp._source[key], silentAndLocal);
});
};
return doc.fetch().then(function initDoc(resp) {
if (!resp.found) {
return doc.doIndex({
buildNum: buildNum
}).then(getDoc);
} else {
// apply update, and keep it quiet the first time
applyMassUpdate(resp, true);
// don't keep it quiet other times
doc.onUpdate(function (resp) {
applyMassUpdate(resp, false);
});
}
});
}())
.then(function () {
$rootScope.$broadcast('init:config');
})
.then(complete, complete.failure);
});
config.get = function (key, defaultVal) {
let keyVal;
if (vals[key] == null) {
if (defaultVal == null) {
keyVal = defaults[key].value;
} else {
keyVal = _.cloneDeep(defaultVal);
}
} else {
keyVal = vals[key];
}
if (defaults[key] && defaults[key].type === 'json') {
return JSON.parse(keyVal);
}
return keyVal;
};
// sets a value in the config
config.set = function (key, val) {
if (_.isPlainObject(val)) {
return change(key, angular.toJson(val));
} else {
return change(key, val);
}
};
// clears a value from the config
config.clear = function (key) {
return change(key);
};
// alias for clear
config.delete = config.clear;
config.close = function () {
if (updater) updater.fire();
};
config.getAll = () => cloneDeep(settings);
config.get = key => getCurrentValue(key);
config.set = (key, val) => change(key, isPlainObject(val) ? angular.toJson(val) : val);
config.remove = key => change(key, null);
config.isDefault = key => !(key in settings) || nullOrEmpty(settings[key].userValue);
config.isCustom = key => key in settings && !('value' in settings[key]);
config.watchAll = (fn, scope) => watchAll(scope, fn);
config.watch = (key, fn, scope) => watch(key, scope, fn);
/**
* A little helper for binding config variables to $scopes
@ -129,34 +32,94 @@ module.service('config', function (Private, kbnVersion, kbnIndex, $rootScope, bu
* be stored. Defaults to the config key
* @return {function} - an unbind function
*/
config.$bind = function ($scope, key, property) {
if (!property) property = key;
let update = function () {
$scope[property] = config.get(key);
};
update();
return _.partial(_.invoke, [
$scope.$on('change:config.' + key, update),
$scope.$on('init:config', update)
], 'call');
config.bindToScope = function (scope, key, property = key) {
return watch(key, scope, update);
function update(newVal) {
scope[property] = newVal;
}
};
/*****
* PRIVATE API
*****/
function change(key, val, silentAndLocal) {
// if the previous updater has already fired, then start over with null
if (updater && updater.fired) updater = null;
// create a new updater
if (!updater) updater = new DelayedUpdater(doc);
// return a promise that will be resolved once the action is eventually done
return updater.update(key, val, silentAndLocal);
function watch(key, scope = $rootScope, fn) {
if (!(key in settings)) {
throw new Error(`Unexpected \`config.watch("${key}", fn)\` call on unrecognized configuration setting "${key}".
Setting an initial value via \`config.set("${key}", value)\` before binding
any custom setting configuration watchers for "${key}" may fix this issue.`);
}
const newVal = config.get(key);
const update = (e, ...args) => fn(...args);
fn(newVal, null, key, config);
return scope.$on(`change:config.${key}`, update);
}
config._vals = function () {
return _.cloneDeep(vals);
};
function watchAll(scope = $rootScope, fn) {
const update = (e, ...args) => fn(...args);
fn(null, null, null, config);
return scope.$on(`change:config`, update);
}
function change(key, value) {
const oldVal = key in settings ? settings[key].userValue : undefined;
const newVal = key in defaults && defaults[key].defaultValue === value ? null : value;
const unchanged = oldVal === newVal;
if (unchanged) {
return Promise.resolve();
}
const initialVal = config.get(key);
localUpdate(key, newVal);
return delayedUpdate(key, newVal)
.then(updatedSettings => {
settings = mergeSettings(defaults, updatedSettings);
})
.catch(reason => {
localUpdate(key, initialVal);
notify.error(reason);
});
}
function localUpdate(key, newVal) {
const oldVal = config.get(key);
patch(key, newVal);
advertise(key, oldVal);
}
function patch(key, value) {
if (!(key in settings)) {
settings[key] = {};
}
if (value === null) {
delete settings[key].userValue;
} else {
settings[key].userValue = value;
}
}
function advertise(key, oldVal) {
const newVal = config.get(key);
notify.log(`config change: ${key}: ${oldVal} -> ${newVal}`);
$rootScope.$broadcast(`change:config.${key}`, newVal, oldVal, key, config);
$rootScope.$broadcast(`change:config`, newVal, oldVal, key, config);
}
function nullOrEmpty(value) {
return value === undefined || value === null;
}
function getCurrentValue(key) {
if (!(key in settings)) {
throw new Error(`Unexpected \`config.get("${key}")\` call on unrecognized configuration setting "${key}".
Setting an initial value via \`config.set("${key}", value)\` before attempting to retrieve
any custom setting value for "${key}" may fix this issue.`);
}
const { userValue, value: defaultValue, type } = settings[key];
const currentValue = config.isDefault(key) ? defaultValue : userValue;
if (type === 'json') {
return JSON.parse(currentValue);
}
return currentValue;
}
});
function mergeSettings(extended, defaults) {
return defaultsDeep(extended, defaults);
}

View file

@ -310,7 +310,7 @@ export default function SourceAbstractFactory(Private, Promise, PromiseEmitter)
/**
* Translate a filter into a query to support es 3+
* @param {Object} filter - The fitler to translate
* @param {Object} filter - The filter to translate
* @return {Object} the query version of that filter
*/
let translateToQuery = function (filter) {

View file

@ -128,7 +128,7 @@ describe('typeahead directive', function () {
expect($typeaheadScope.items.length).to.be(typeaheadItems.length);
});
it('should order fitlered results', function () {
it('should order filtered results', function () {
let entries = ['ac/dc', 'anthrax', 'abba', 'phantogram', 'skrillex'];
let allEntries = typeaheadItems.concat(entries);
let startMatches = allEntries.filter(function (item) {

View file

@ -1,3 +1,4 @@
import $ from 'jquery';
import 'ui/filters/short_dots';
import uiModules from 'ui/modules';
let module = uiModules.get('kibana');
@ -52,11 +53,14 @@ module.directive('fieldName', function ($compile, $rootScope, $filter) {
let displayName = $filter('shortDots')(name);
$el
.text(displayName)
.attr('title', name)
.toggleClass('no-results', results)
.toggleClass('scripted', scripted)
.prepend(typeIcon(type));
.prepend(typeIcon(type))
.append($('<span>')
.text(displayName)
.addClass('discover-field-name')
);
});
}
};

View file

@ -55,7 +55,7 @@ export default function mapFilterProvider(Promise, Private) {
/**
* Map the filter into an object with the key and value exposed so it's
* easier to work with in the template
* @param {object} fitler The filter the map
* @param {object} filter The filter the map
* @returns {Promise}
*/
return function (filter) {

View file

@ -6,7 +6,7 @@ uiModules
.get('kibana')
.filter('moment', function (config) {
return function (datetime) {
let format = config.get('dateFormat');
const format = config.get('dateFormat');
if (moment.isMoment(datetime)) return datetime.format(format);
if (_.isDate(datetime)) return moment(datetime).format(format);
return datetime;

View file

@ -10,20 +10,19 @@ uiModules
return Private(shortDotsFilterProvider);
});
function shortDotsFilterProvider(config, $rootScope) {
function shortDotsFilterProvider(config) {
let filter;
function updateFilter() {
filter = config.get('shortDots:enable') ? _.shortenDottedString : _.identity;
config.watch('shortDots:enable', updateFilter);
return wrapper;
function updateFilter(enabled) {
filter = enabled ? _.shortenDottedString : _.identity;
}
updateFilter();
$rootScope.$on('change:config.shortDots:enable', updateFilter);
$rootScope.$on('init:config', updateFilter);
return function (str) {
function wrapper(str) {
return filter(str);
};
}
}
export default shortDotsFilterProvider;

View file

@ -1,11 +1,11 @@
import _ from 'lodash';
// Takes a hit, merges it with any stored/scripted fields, and with the metaFields
// returns a flattened version
export default function FlattenHitProvider(config, $rootScope) {
export default function FlattenHitProvider(config) {
let metaFields = config.get('metaFields');
$rootScope.$on('change:config.metaFields', function () {
metaFields = config.get('metaFields');
config.watch('metaFields', value => {
metaFields = value;
});
function flattenHit(indexPattern, hit) {
@ -47,7 +47,7 @@ export default function FlattenHitProvider(config, $rootScope) {
return flat;
}
return function (indexPattern) {
return function flattenHitWrapper(indexPattern) {
function cachedFlatten(hit) {
return hit.$$_flattened || (hit.$$_flattened = flattenHit(indexPattern, hit));
}

View file

@ -13,8 +13,7 @@ import IndexPatternsFieldListProvider from 'ui/index_patterns/_field_list';
import IndexPatternsFlattenHitProvider from 'ui/index_patterns/_flatten_hit';
import IndexPatternsCalculateIndicesProvider from 'ui/index_patterns/_calculate_indices';
import IndexPatternsPatternCacheProvider from 'ui/index_patterns/_pattern_cache';
export default function IndexPatternFactory(Private, timefilter, Notifier, config, kbnIndex, Promise, $rootScope, safeConfirm) {
export default function IndexPatternFactory(Private, timefilter, Notifier, config, kbnIndex, Promise, safeConfirm) {
let fieldformats = Private(RegistryFieldFormatsProvider);
let getIds = Private(IndexPatternsGetIdsProvider);
let mapper = Private(IndexPatternsMapperProvider);
@ -76,8 +75,11 @@ export default function IndexPatternFactory(Private, timefilter, Notifier, confi
.id(self.id);
// listen for config changes and update field list
$rootScope.$on('change:config', function () {
initFields();
config.watchAll(() => {
if (self.fields) {
// re-init fields when config changes, but only if we already had fields
initFields();
}
});
return mappingSetup.isDefined(type)

View file

@ -20,10 +20,7 @@ module.exports = function (opts) {
let rootSearchSource = Private(CourierDataSourceRootSearchSourceProvider);
let path = _.get($route, 'current.$$route.originalPath');
return config.init()
.then(function () {
return getIds();
})
return getIds()
.then(function (patterns) {
let defaultId = config.get('defaultIndex');
let defined = !!defaultId;
@ -31,7 +28,7 @@ module.exports = function (opts) {
let required = !notRequiredRe || !path.match(notRequiredRe);
if (defined && !exists) {
config.clear('defaultIndex');
config.remove('defaultIndex');
defaultId = defined = false;
}

View file

@ -119,6 +119,8 @@ function add(notif, cb) {
return notif;
}
Notifier.prototype.add = add;
function formatInfo() {
let info = [];

View file

@ -31,21 +31,14 @@ module.run(function ($interval) {
// expect access to config (since it's dependent on kibana)
if (!!kbnIndex) {
require('ui/config');
module.run(function ($rootScope, config) {
let configInitListener = $rootScope.$on('init:config', function () {
applyConfig();
configInitListener();
});
$rootScope.$on('change:config', applyConfig);
function applyConfig() {
module.run(function (config) {
config.watchAll(() => {
Notifier.applyConfig({
errorLifetime: config.get('notifications:lifetime:error'),
warningLifetime: config.get('notifications:lifetime:warning'),
infoLifetime: config.get('notifications:lifetime:info')
});
}
});
});
}

View file

@ -6,14 +6,12 @@ export default uiRegistry({
index: ['id'],
group: ['fieldType'],
constructor: function (config, $rootScope) {
constructor: function (config) {
let self = this;
let defaultMap;
function init() {
parseDefaultTypeMap();
$rootScope.$on('init:config', parseDefaultTypeMap);
$rootScope.$on('change:config.format:defaultTypeMap', parseDefaultTypeMap);
config.watch('format:defaultTypeMap', parseDefaultTypeMap);
}
@ -73,8 +71,8 @@ export default uiRegistry({
});
function parseDefaultTypeMap() {
defaultMap = config.get('format:defaultTypeMap');
function parseDefaultTypeMap(value) {
defaultMap = value;
_.forOwn(self, function (fn) {
if (_.isFunction(fn) && fn.cache) {
// clear all memoize caches

View file

@ -8,7 +8,7 @@ let fieldFormats;
let FieldFormat;
let config;
let formatIds = [
const formatIds = [
'bytes',
'date',
'duration',
@ -73,7 +73,7 @@ module.exports = describe('conformance', function () {
basicPatternTests('number', require('numeral'))();
it('tries to parse strings', function () {
let number = new (fieldFormats.getType('number'))({ pattern: '0.0b' });
const number = new (fieldFormats.getType('number'))({ pattern: '0.0b' });
expect(number.convert(123.456)).to.be('123.5B');
expect(number.convert('123.456')).to.be('123.5B');
});
@ -81,11 +81,11 @@ module.exports = describe('conformance', function () {
});
function basicPatternTests(id, lib) {
let confKey = id === 'date' ? 'dateFormat' : 'format:' + id + ':defaultPattern';
const confKey = id === 'date' ? 'dateFormat' : 'format:' + id + ':defaultPattern';
return function () {
it('converts using the format:' + id + ':defaultPattern config', function () {
let inst = fieldFormats.getInstance(id);
const inst = fieldFormats.getInstance(id);
[
'0b',
'0 b',
@ -93,26 +93,31 @@ module.exports = describe('conformance', function () {
'0.[000]b',
'0.[0]b'
].forEach(function (pattern) {
let num = _.random(-10000, 10000, true);
const original = config.get(confKey);
const num = _.random(-10000, 10000, true);
config.set(confKey, pattern);
expect(inst.convert(num)).to.be(lib(num).format(pattern));
config.set(confKey, original);
});
});
it('uses the pattern param if available', function () {
let num = _.random(-10000, 10000, true);
let defFormat = '0b';
let customFormat = '0.00000%';
const original = config.get(confKey);
const num = _.random(-10000, 10000, true);
const defFormat = '0b';
const customFormat = '0.00000%';
config.set(confKey, defFormat);
let defInst = fieldFormats.getInstance(id);
const defInst = fieldFormats.getInstance(id);
let Type = fieldFormats.getType(id);
let customInst = new Type({ pattern: customFormat });
const Type = fieldFormats.getType(id);
const customInst = new Type({ pattern: customFormat });
expect(defInst.convert(num)).to.not.be(customInst.convert(num));
expect(defInst.convert(num)).to.be(lib(num).format(defFormat));
expect(customInst.convert(num)).to.be(lib(num).format(customFormat));
config.set(confKey, original);
});
};
}

View file

@ -2,16 +2,20 @@ import expect from 'expect.js';
import ngMock from 'ng_mock';
import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
describe('IP Address Format', function () {
let fieldFormats;
let ip;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
fieldFormats = Private(RegistryFieldFormatsProvider);
const fieldFormats = Private(RegistryFieldFormatsProvider);
ip = fieldFormats.getInstance('ip');
}));
it('converts a value from a decimal to a string', function () {
let ip = fieldFormats.getInstance('ip');
expect(ip.convert(1186489492)).to.be('70.184.100.148');
});
it('converts null and undefined to -', function () {
expect(ip.convert(null)).to.be('-');
expect(ip.convert(undefined)).to.be('-');
});
});

View file

@ -13,6 +13,7 @@ export default function IpFormatProvider(Private) {
Ip.fieldType = 'ip';
Ip.prototype._convert = function (val) {
if (val === undefined || val === null) return '-';
if (!isFinite(val)) return val;
// shazzam!

View file

@ -2,7 +2,7 @@ import modules from 'ui/modules';
modules.get('kibana').config(function ($provide) {
$provide.decorator('timefilter', function ($delegate) {
$delegate.consumeDefaults();
$delegate.init();
return $delegate;
});
});

View file

@ -38,11 +38,6 @@ uiModules
self.enabled = false;
self.init = _.once(function () {
return config.init()
.then(self.consumeDefaults);
});
self.consumeDefaults = _.once(function () {
let timeDefaults = config.get('timepicker:timeDefaults');
let refreshIntervalDefaults = config.get('timepicker:refreshIntervalDefaults');

View file

@ -19,7 +19,7 @@ UiModules
return {
template: toggleHtml,
link: ($scope, $el, attrs) => {
config.$bind($scope, 'dateFormat:dow', 'dateFormat_dow');
config.bindToScope($scope, 'dateFormat:dow', 'dateFormat_dow');
$scope.$watch('dateFormat_dow', function (day) {
const dow = moment.weekdays().indexOf(day);
moment.locale(moment.locale(), { week: { dow } });

View file

@ -1,7 +1,6 @@
import moment from 'moment-timezone';
import _ from 'lodash';
export default function configDefaultsProvider() {
export default function defaultSettingsProvider() {
const weekdays = moment.weekdays().slice();
const [defaultWeekday] = weekdays;
@ -28,25 +27,26 @@ export default function configDefaultsProvider() {
value: 'Browser',
description: 'Which timezone should be used. "Browser" will use the timezone detected by your browser.',
type: 'select',
options: _.union(['Browser'], moment.tz.names())
options: ['Browser', ...moment.tz.names()]
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "HH:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are' +
' <a href="http://en.wikipedia.org/wiki/ISO_8601#Time_intervals" target="_blank">' +
'ISO8601 intervals.</a>'
`[
["", "HH:mm:ss.SSS"],
["PT1S", "HH:mm:ss"],
["PT1M", "HH:mm"],
["PT1H", "YYYY-MM-DD HH:mm"],
["P1DT", "YYYY-MM-DD"],
["P1YT", "YYYY"]
]`,
description: (
'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are' +
' <a href="http://en.wikipedia.org/wiki/ISO_8601#Time_intervals" target="_blank">' +
'ISO8601 intervals.</a>'
)
},
'dateFormat:dow': {
value: defaultWeekday,
@ -113,14 +113,14 @@ export default function configDefaultsProvider() {
attribution: 'Maps provided by USGS',
styles: '',
}
}, null, ' '),
}, null, 2),
type: 'json',
description: 'Default <a href="http://leafletjs.com/reference.html#tilelayer-wms" target="_blank">properties</a> for the WMS map server support in the tile map'
},
'visualization:colorMapping': {
type: 'json',
value: JSON.stringify({
'Count': '#6eadc1'
Count: '#6eadc1'
}),
description: 'Maps values to specified colors within visualizations'
},
@ -155,15 +155,14 @@ export default function configDefaultsProvider() {
},
'format:defaultTypeMap': {
type: 'json',
value: [
'{',
' "ip": { "id": "ip", "params": {} },',
' "date": { "id": "date", "params": {} },',
' "number": { "id": "number", "params": {} },',
' "_source": { "id": "_source", "params": {} },',
' "_default_": { "id": "string", "params": {} }',
'}',
].join('\n'),
value:
`{
"ip": { "id": "ip", "params": {} },
"date": { "id": "date", "params": {} },
"number": { "id": "number", "params": {} },
"_source": { "id": "_source", "params": {} },
"_default_": { "id": "string", "params": {} }
}`,
description: 'Map of the format name to use by default for each field type. ' +
'"_default_" is used if the field type is not mentioned explicitly'
},
@ -194,24 +193,22 @@ export default function configDefaultsProvider() {
},
'timepicker:timeDefaults': {
type: 'json',
value: [
'{',
' "from": "now-15m",',
' "to": "now",',
' "mode": "quick"',
'}'
].join('\n'),
value:
`{
"from": "now-15m",
"to": "now",
"mode": "quick"
}`,
description: 'The timefilter selection to use when Kibana is started without one'
},
'timepicker:refreshIntervalDefaults': {
type: 'json',
value: [
'{',
' "display": "Off",',
' "pause": false,',
' "value": 0',
'}'
].join('\n'),
value:
`{
"display": "Off",
"pause": false,
"value": 0
}`,
description: 'The timefilter\'s default refresh interval'
},
'dashboard:defaultDarkTheme': {

71
src/ui/settings/index.js Normal file
View file

@ -0,0 +1,71 @@
import { defaultsDeep } from 'lodash';
import defaultsProvider from './defaults';
export default function setupSettings(kbnServer, server, config) {
const uiSettings = {
getAll,
getDefaults,
getUserProvided,
set,
setMany,
remove
};
server.decorate('server', 'uiSettings', () => uiSettings);
function getAll() {
return Promise
.all([getDefaults(), getUserProvided()])
.then(([defaults, user]) => defaultsDeep(user, defaults));
}
function getDefaults() {
return Promise.resolve(defaultsProvider());
}
function getUserProvided() {
const { client } = server.plugins.elasticsearch;
const clientSettings = getClientSettings(config);
return client
.get({ ...clientSettings })
.then(res => res._source)
.then(user => hydrateUserSettings(user));
}
function setMany(changes) {
const { client } = server.plugins.elasticsearch;
const clientSettings = getClientSettings(config);
return client
.update({
...clientSettings,
body: { doc: changes }
})
.then(() => ({}));
}
function set(key, value) {
return setMany({ [key]: value });
}
function remove(key) {
return set(key, null);
}
}
function hydrateUserSettings(user) {
return Object.keys(user).reduce(expand, {});
function expand(expanded, key) {
const userValue = user[key];
if (userValue !== null) {
expanded[key] = { userValue };
}
return expanded;
}
}
function getClientSettings(config) {
const index = config.get('kibana.index');
const id = config.get('pkg.version');
const type = 'config';
return { index, type, id };
}

View file

@ -26,4 +26,9 @@ export default class UiNavLinkCollection extends Collection {
return this[inOrderCache];
}
delete(value) {
this[inOrderCache] = null;
return super.delete(value);
}
};

View file

@ -14,7 +14,7 @@ module.exports = function (grunt) {
// TODO(sissel): Check if `fpm` is available
if (!(/linux-x(86|64)$/.test(name))) return;
const arch = /x64$/.test(name) ? 'x86_64' : 'i686';
const arch = /x64$/.test(name) ? 'x86_64' : 'i386';
const fpm = args => exec('fpm', args);
const args = [

View file

@ -16,6 +16,7 @@ module.exports = function createServices(grunt) {
'--install-prefix', service.outputDir,
'--overwrite',
'--user', 'kibana',
'--group', 'kibana',
'--sysv-log-path', '/var/log/kibana/',
'-p', service.name,
'-v', service.version,
@ -25,6 +26,6 @@ module.exports = function createServices(grunt) {
grunt.file.mkdir(userScriptsDir);
exec('please-manage-user', ['--output', userScriptsDir, 'kibana']);
appendFileSync(resolve(userScriptsDir, 'installer.sh'), 'chown kibana /opt/kibana/optimize');
appendFileSync(resolve(userScriptsDir, 'installer.sh'), 'chown kibana:kibana /opt/kibana/optimize');
});
};

View file

@ -10,8 +10,8 @@ module.exports = function (grunt) {
readdir(targetDir)
.map(function (archive) {
// only sha the archives
if (!archive.match(/\.zip$|\.tar.gz$/)) return;
// only sha the archives and packages
if (!archive.match(/\.zip$|\.tar.gz$|\.deb$|\.rpm$/)) return;
return exec(cmd + archive + ' > ' + archive + '.sha1.txt', {
cwd: targetDir

20
tasks/config/packages.js Normal file
View file

@ -0,0 +1,20 @@
export default (grunt) => {
const version = grunt.config.get('pkg.version');
const productionPath = `kibana/${version.match(/\d\.\d/)[0]}`;
const stagingPath = `kibana/staging/${version.match(/\d\.\d\.\d/)[0]}-XXXXXXX/repos/${version.match(/\d\./)[0]}x`;
const rpmFolder = 'centos';
const debFolder = 'debian';
return {
staging: {
bucket: 'download.elasticsearch.org',
debPrefix: `${stagingPath}/${debFolder}`,
rpmPrefix: `${stagingPath}/${rpmFolder}`
},
production: {
bucket: 'packages.elasticsearch.org',
debPrefix: `${productionPath}/${debFolder}`,
rpmPrefix: `${productionPath}/${rpmFolder}`
}
};
};

View file

@ -26,12 +26,27 @@ module.exports = function (grunt) {
let zipName = `${buildName}.zip`;
let zipPath = resolve(rootPath, `target/${zipName}`);
let debName;
let debPath;
let rpmName;
let rpmPath;
if (name.match('linux')) {
let debArch = name.match('x64') ? 'amd64' : 'i386';
debName = `kibana_${version}_${debArch}.deb`;
debPath = resolve(rootPath, `target/${debName}`);
let rpmArch = name.match('x64') ? 'x86_64' : 'i386';
rpmName = `kibana-${version.replace('-', '_')}-1.${rpmArch}.rpm`;
rpmPath = resolve(rootPath, `target/${rpmName}`);
}
return {
name, win,
nodeUrl, nodeDir,
buildName, buildDir,
tarName, tarPath,
zipName, zipPath,
debName, debPath,
rpmName, rpmPath
};
});
};

View file

@ -1,4 +1,3 @@
var _ = require('lodash');
module.exports = function (grunt) {
var { config } = grunt;
@ -6,22 +5,7 @@ module.exports = function (grunt) {
release: {
bucket: 'download.elasticsearch.org',
access: 'private',
debug: false,
upload: config.get('platforms')
.reduce(function (files, platform) {
return files.concat(
platform.tarName,
platform.tarName + '.sha1.txt',
platform.zipName,
platform.zipName + '.sha1.txt'
);
}, [])
.map(function (filename) {
return {
src: 'target/' + filename,
dest: 'kibana/kibana/' + filename
};
})
debug: false
}
};
};

36
tasks/rebuild/confirm.js Normal file
View file

@ -0,0 +1,36 @@
import { execFileSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { createInterface } from 'readline';
export default (grunt) => {
grunt.registerTask('_rebuild:confirm', function () {
const newVersion = grunt.option('buildversion') || grunt.config.get('pkg').version;
const newBuildNum = grunt.option('buildnum') || grunt.config.get('buildNum');
const newSha = grunt.option('buildsha') || grunt.config.get('buildSha');
grunt.config('rebuild', { newVersion, newBuildNum, newSha });
grunt.log.writeln('Targets will be rebuilt with the following:');
grunt.log.writeln(`Version: ${newVersion}`);
grunt.log.writeln(`Build number: ${newBuildNum}`);
grunt.log.writeln(`Build sha: ${newSha}`);
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('close', this.async());
rl.question('Do you want to rebuild these packages? [N/y] ', (resp) => {
const answer = resp.toLowerCase().trim();
if (answer === 'y') {
grunt.config.set('rebuild.continue', true);
}
rl.close();
});
});
};

View file

@ -0,0 +1,23 @@
import { execFileSync } from 'child_process';
import { join } from 'path';
export default (grunt) => {
grunt.registerTask('_rebuild:createArchives', function () {
const buildDir = grunt.config.get('build');
const targetDir = grunt.config.get('target');
grunt.file.mkdir('target');
grunt.file.expand({ cwd: buildDir }, '*').forEach(build => {
const tar = join(targetDir, `${build}.tar.gz`);
execFileSync('tar', ['-zchf', tar, build], { cwd: buildDir });
const zip = join(targetDir, `${build}.zip`);
if (/windows/.test(build)) {
execFileSync('zip', ['-rq', '-ll', zip, build], { cwd: buildDir });
} else {
execFileSync('zip', ['-rq', zip, build], { cwd: buildDir });
}
});
});
};

View file

@ -0,0 +1,14 @@
import { execFileSync } from 'child_process';
export default (grunt) => {
grunt.registerTask('_rebuild:extractZips', function () {
const buildDir = grunt.config.get('build');
const targetDir = grunt.config.get('target');
const zips = grunt.file.expand({ cwd: targetDir }, '*.zip');
zips.forEach(zip => {
execFileSync('unzip', [zip, '-d', buildDir], { cwd: targetDir });
});
});
};

66
tasks/rebuild/index.js Normal file
View file

@ -0,0 +1,66 @@
import { execSync } from 'child_process';
import { trim } from 'lodash';
/**
* Repackages all of the current archives in target/ with the same build
* number, sha, and commit hash. This is useful when all you need to do is bump
* the version of the release and do not want to introduce any other changes.
*
* Even if there are new commits, the standard build task reinstalls all npm
* dependencies, which introduces at least a small amount of risk of
* introducing bugs into the build since not all dependencies have fixed
* versions.
*
* Options:
* --skip-archives Will skip the archive step, useful for debugging
* --buildversion="1.2.3" Sets new version to 1.2.3
* --buildnum="99999" Sets new build number to 99999
* --buildsha="9a5b2c1" Sets new build sha to 9a5b2c1 (use the full sha, though)
*/
export default (grunt) => {
grunt.registerTask('rebuild', 'Rebuilds targets as a new version', function () {
grunt.task.run([
'_build:getProps',
'_rebuild:confirm',
'_rebuild:continue'
]);
});
grunt.registerTask('_rebuild:continue', function () {
grunt.task.requires('_rebuild:confirm');
if (!grunt.config.get('rebuild.continue')) {
grunt.log.writeln('Aborting without rebuilding anything');
} else {
grunt.task.run([
'_rebuild:builds',
'_rebuild:archives'
]);
}
});
grunt.registerTask('_rebuild:builds', function () {
grunt.task.requires('_rebuild:continue');
grunt.task.run([
'clean:build',
'_rebuild:extractZips',
'_rebuild:updateBuilds'
]);
});
grunt.registerTask('_rebuild:archives', function () {
grunt.task.requires('_rebuild:continue');
const skip = grunt.option('skip-archives');
if (skip) {
grunt.log.writeln('Skipping archive step since rebuild debugging was enabled');
} else {
grunt.task.run([
'clean:target',
'_rebuild:createArchives',
'_build:shasums'
]);
}
});
};

View file

@ -0,0 +1,57 @@
import { execFileSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
export default (grunt) => {
grunt.registerTask('_rebuild:updateBuilds', function () {
const buildDir = grunt.config.get('build');
const { newVersion, newBuildNum, newSha } = grunt.config.get('rebuild');
grunt.file.expand({ cwd: buildDir }, '*').forEach(build => {
const thisBuildDir = join(buildDir, build);
const thisBundlesDir = join(thisBuildDir, 'optimize', 'bundles');
const readmePath = join(thisBuildDir, 'README.txt');
const pkgjsonPath = join(thisBuildDir, 'package.json');
const bundlePaths = [
...grunt.file.expand({ cwd: thisBundlesDir }, '*.bundle.js'),
...grunt.file.expand({ cwd: thisBundlesDir }, '*.entry.js')
];
const { oldBuildNum, oldSha, oldVersion } = readBuildInfo(pkgjsonPath);
replaceIn(readmePath, oldVersion, newVersion);
replaceIn(pkgjsonPath, oldVersion, newVersion);
replaceIn(pkgjsonPath, `"number": ${oldBuildNum},`, `"number": ${newBuildNum},`);
replaceIn(pkgjsonPath, oldSha, newSha);
bundlePaths
.map(bundle => join(thisBundlesDir, bundle))
.forEach(file => {
replaceIn(file, `"kbnVersion":"${oldVersion}"`, `"kbnVersion":"${newVersion}"`);
replaceIn(file, `"buildNum":${oldBuildNum}`, `"buildNum":${newBuildNum}`);
});
const newBuild = build.replace(oldVersion, newVersion);
if (build !== newBuild) {
execFileSync('mv', [ build, newBuild ], { cwd: buildDir });
}
});
});
};
function readBuildInfo(path) {
const pkgjson = readFileSync(path).toString();
const pkg = JSON.parse(pkgjson);
return {
oldBuildNum: pkg.build.number,
oldSha: pkg.build.sha,
oldVersion: pkg.version
};
}
function replaceIn(path, oldValue, newValue) {
let contents = readFileSync(path).toString();
contents = contents.replace(oldValue, newValue);
writeFileSync(path, contents);
}

View file

@ -1,11 +1,15 @@
module.exports = function (grunt) {
var readline = require('readline');
var url = require('url');
var fs = require('fs');
var _ = require('lodash');
// build, then zip and upload to s3
grunt.registerTask('release', [
'_release:confirmUpload',
'_release:loadS3Config',
'build',
'_release:setS3Uploads',
's3:release',
'_release:complete'
]);
@ -33,18 +37,48 @@ module.exports = function (grunt) {
});
});
grunt.registerTask('_release:setS3Uploads', function () {
var uploads = grunt.config.get('platforms')
.reduce(function (files, platform) {
return files.concat(
platform.tarName,
platform.tarName + '.sha1.txt',
platform.zipName,
platform.zipName + '.sha1.txt',
platform.rpmName,
platform.rpmName && platform.rpmName + '.sha1.txt',
platform.debName,
platform.debName && platform.debName + '.sha1.txt'
);
}, [])
.filter(function (filename) {
if (_.isUndefined(filename)) return false;
try {
fs.accessSync('target/' + filename, fs.F_OK);
return true;
} catch (e) {
return false;
}
})
.map(function (filename) {
return {
src: 'target/' + filename,
dest: 'kibana/kibana/' + filename
};
});
grunt.config.set('s3.release.upload', uploads);
});
grunt.registerTask('_release:complete', function () {
grunt.log.ok('Builds released');
grunt.log.write(
`
${grunt.config.get('platforms').reduce((t, p) => {
return (
`${t}https://download.elastic.co/kibana/kibana/${p.buildName}.tar.gz
https://download.elastic.co/kibana/kibana/${p.buildName}.zip
`
);
}, '')}
`
);
var links = grunt.config.get('s3.release.upload').reduce((t, {dest}) => {
var link = url.format({
protocol: 'https',
hostname: 'download.elastic.co',
pathname: dest
});
return `${t}${link}\n`;
}, '');
grunt.log.write(links);
});
};

113
tasks/release_packages.js Normal file
View file

@ -0,0 +1,113 @@
import exec from './utils/exec';
import SimpleGit from 'simple-git';
import { promisify } from 'bluebird';
import readline from 'readline';
export default (grunt) => {
const packages = grunt.config.get('packages');
const platforms = grunt.config.get('platforms');
function debS3(deb) {
exec('deb-s3', [
'upload',
'--preserve-versions',
deb.filePath,
'--bucket', deb.bucket,
'--prefix', deb.prefix,
'--sign', deb.signatureKeyId,
'--arch', deb.arch,
`--access-key-id=${deb.awsKey}`,
`--secret-access-key=${deb.awsSecret}`
]);
}
function rpmS3(rpm) {
exec('rpm', [
'--resign', rpm.filePath,
'--define', '_signature gpg',
'--define', `_gpg_name ${rpm.signingKeyName}`
]);
exec('rpm-s3', [
'-v',
'-b', rpm.bucket,
'-p', rpm.prefix,
'--sign',
'--visibility', 'public-read',
'-k', '100',
rpm.filePath,
'-r', 'external-1'
], {
env: Object.assign({}, {
'AWS_ACCESS_KEY': rpm.awsKey,
'AWS_SECRET_KEY': rpm.awsSecret
}, process.env)
});
}
grunt.registerTask('publishPackages:staging', [
'_publishPackages:confirm',
'_publishPackages:upload:staging',
]);
grunt.registerTask('publishPackages:production', [
'_publishPackages:confirm',
'_publishPackages:upload:production',
]);
grunt.registerTask('_publishPackages:confirm', function () {
function abort() {
grunt.fail.fatal('Aborting publish');
}
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('close', this.async());
rl.on('SIGINT', () => abort());
rl.question('Publish packages to s3? [N/y] ', function (resp) {
if (resp.toLowerCase().trim()[0] === 'y') return rl.close();
abort();
});
});
grunt.registerTask('_publishPackages:upload', function (environment) {
const aws = grunt.file.readJSON('.aws-config.json');
const signature = grunt.file.readJSON('.signing-config.json');
const done = this.async();
const simpleGit = new SimpleGit();
const revparse = promisify(simpleGit.revparse, simpleGit);
return revparse(['--short', 'HEAD'])
.then(hash => {
const trimmedHash = hash.trim();
platforms.forEach((platform) => {
if (platform.debPath) {
debS3({
filePath: platform.debPath,
bucket: packages[environment].bucket,
prefix: packages[environment].debPrefix.replace('XXXXXXX', trimmedHash),
signatureKeyId: signature.id,
arch: platform.name.match('x64') ? 'amd64' : 'i386',
awsKey: aws.key,
awsSecret: aws.secret
});
}
if (platform.rpmPath) {
rpmS3({
filePath: platform.rpmPath,
bucket: packages[environment].bucket,
prefix: packages[environment].rpmPrefix.replace('XXXXXXX', trimmedHash),
signingKeyName: signature.name,
awsKey: aws.key,
awsSecret: aws.secret
});
}
});
done();
});
});
};

View file

@ -42,13 +42,9 @@ import {
.catch(common.handleError(this));
});
bdd.it('default request reponse should contain .kibana' , function () {
bdd.it('default request response should contain .kibana' , function () {
var expectedResponseContains = '"_index": ".kibana",';
var elasticsearch = common.getEsHostPort();
return consolePage.setServer(elasticsearch)
.then(function () {
return consolePage.clickPlay();
})
return consolePage.clickPlay()
.then(function () {
return common.try(function () {
return consolePage.getResponse()
@ -62,7 +58,6 @@ import {
});
});
}());
}());

View file

@ -94,7 +94,7 @@ import {
bdd.it('should show the correct hit count', function () {
var expectedHitCount = '14,004';
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getHitCount()
.then(function compareData(hitCount) {
expect(hitCount).to.be(expectedHitCount);
@ -243,7 +243,7 @@ import {
function verifyChartData(expectedBarChartData) {
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getBarChartData()
.then(function compareData(paths) {
// the largest bars are over 100 pixels high so this is less than 1% tolerance

View file

@ -51,7 +51,7 @@ import {
var expectedHitCount = '445';
return discoverPage.query('php')
.then(function () {
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getHitCount()
.then(function compareData(hitCount) {
expect(hitCount).to.be(expectedHitCount);
@ -76,7 +76,7 @@ import {
var expectedHitCount = '11,156';
return discoverPage.query('_type:apache')
.then(function () {
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getHitCount()
.then(function compareData(hitCount) {
expect(hitCount).to.be(expectedHitCount);
@ -229,7 +229,7 @@ import {
return common.sleep(2000);
})
.then(function () {
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getDocTableIndex(1)
.then(function (rowData) {
expect(rowData).to.be(ExpectedDoc);

View file

@ -102,7 +102,7 @@ import { bdd, common, discoverPage, headerPage, settingsPage, scenarioManager }
var re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$');
return discoverPage.clickShortenUrl()
.then(function () {
return common.tryForTime(20 * 1000, function tryingForTime() {
return common.try(function tryingForTime() {
return discoverPage.getShortenedUrl()
.then(function (actualUrl) {
expect(actualUrl).to.match(re);

View file

@ -32,7 +32,7 @@ import {
});
bdd.it('should have index pattern in url', function url() {
return common.tryForTime(5000, function () {
return common.try(function tryingForTime() {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.contain('logstash-*');
@ -80,7 +80,7 @@ import {
});
bdd.it('should return to the add data landing page', function returnToPage() {
return common.tryForTime(5000, function () {
return common.try(function tryingForTime() {
return common.findTestSubject('addData');
})
.catch(common.handleError(this));
@ -88,7 +88,7 @@ import {
bdd.it('should remove index pattern from url', function indexNotInUrl() {
// give the url time to settle
return common.tryForTime(5000, function () {
return common.try(function tryingForTime() {
return remote.getCurrentUrl()
.then(function (currentUrl) {
common.debug('currentUrl = ' + currentUrl);

View file

@ -123,7 +123,7 @@ import {
683, 1361, 1415, 707, 177, 27, 32, 175, 707, 1408, 1355, 726, 201, 29
];
return common.tryForTime(5000, function () {
return common.try(function tryingForTime() {
return visualizePage.getXAxisLabels()
.then(function compareLabels(labels) {
common.debug('X-Axis labels = ' + labels);

View file

@ -65,7 +65,7 @@ import {
var expectedCount = ['14,004', 'Count'];
// initial metric of "Count" is selected by default
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(expectedCount).to.eql(metricValue.split('\n'));
@ -88,7 +88,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(avgMachineRam).to.eql(metricValue.split('\n'));
@ -110,7 +110,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(sumPhpMemory).to.eql(metricValue.split('\n'));
@ -133,7 +133,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
// only comparing the text label!
@ -156,7 +156,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(minTimestamp).to.eql(metricValue.split('\n'));
@ -178,7 +178,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(maxRelatedContentArticleModifiedTime).to.eql(metricValue.split('\n'));
@ -204,7 +204,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(standardDeviationBytes).to.eql(metricValue.split('\n'));
@ -226,7 +226,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(uniqueCountClientip).to.eql(metricValue.split('\n'));
@ -264,7 +264,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(percentileMachineRam).to.eql(metricValue.split('\n'));
@ -290,7 +290,7 @@ import {
return visualizePage.clickGo();
})
.then(function () {
return common.tryForTime(2000, function () {
return common.try(function tryingForTime() {
return visualizePage.getMetric()
.then(function (metricValue) {
expect(percentileRankBytes).to.eql(metricValue.split('\n'));

View file

@ -61,7 +61,7 @@ import {
})
.then(function () {
common.debug('Click field geo.coordinates');
return common.tryForTime(1000, function () {
return common.try(function tryingForTime() {
return visualizePage.selectField('geo.coordinates');
});
})

View file

@ -20,6 +20,7 @@ define(function (require) {
excludeInstrumentation: /.*/,
defaultTimeout: 90000,
defaultTryTimeout: 40000, // tryForTime could include multiple 'find timeouts'
defaultFindTimeout: 10000 // this is how long we try to find elements on page
}, serverConfig);
});

View file

@ -14,6 +14,7 @@ exports.bdd = kbnInternVars.bdd;
exports.intern = kbnInternVars.intern;
exports.config = exports.intern.config;
exports.defaultTimeout = exports.config.defaultTimeout;
exports.defaultTryTimeout = exports.config.defaultTryTimeout;
exports.defaultFindTimeout = exports.config.defaultFindTimeout;
exports.scenarioManager = new ScenarioManager(url.format(exports.config.servers.elasticsearch));

View file

@ -1,4 +1,4 @@
import { common, config, defaultFindTimeout, remote, shieldPage } from '../';
import { common, config, defaultTryTimeout, defaultFindTimeout, remote, shieldPage } from '../';
export default (function () {
var Promise = require('bluebird');
@ -225,7 +225,7 @@ export default (function () {
},
try(block) {
return this.tryForTime(defaultFindTimeout, block);
return this.tryForTime(defaultTryTimeout, block);
},
log(...args) {

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