Merge branch 'master' into feature/ingest

This commit is contained in:
Matthew Bargar 2016-06-03 15:53:42 -04:00
commit 0a956057f6
83 changed files with 1565 additions and 492 deletions

View file

@ -27,7 +27,7 @@ authority for your Elasticsearch instance.
to `false`.
`elasticsearch.pingTimeout:`:: *Default: the value of the `elasticsearch.requestTimeout` setting* Time in milliseconds to
wait for Elasticsearch to respond to pings.
`elasticsearch.requestTimeout:`:: *Default: 300000* Time in milliseconds to wait for responses from the back end or
`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait for responses from the back end or
Elasticsearch. This value must be a positive integer.
`elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch.
To send *no* client-side headers, set this value to [] (an empty list).

View file

@ -98,8 +98,10 @@
"css-loader": "0.17.0",
"csv-parse": "1.1.0",
"d3": "3.5.6",
"dragula": "3.7.0",
"elasticsearch": "10.1.2",
"elasticsearch-browser": "10.1.2",
"even-better": "7.0.2",
"expiry-js": "0.1.7",
"exports-loader": "0.6.2",
"expose-loader": "0.7.0",
@ -107,7 +109,6 @@
"file-loader": "0.8.4",
"font-awesome": "4.4.0",
"glob-all": "3.0.1",
"good": "6.3.0",
"good-squeeze": "2.1.0",
"gridster": "0.5.6",
"hapi": "8.8.1",
@ -156,8 +157,10 @@
"auto-release-sinon": "1.0.3",
"babel-eslint": "4.1.8",
"chokidar": "1.4.3",
"elasticdump": "2.1.1",
"eslint": "1.10.3",
"eslint-plugin-mocha": "1.1.0",
"event-stream": "3.3.2",
"expect.js": "0.3.1",
"faker": "1.1.0",
"grunt": "0.4.5",

View file

@ -0,0 +1,6 @@
server:
port: 8274
logging:
json: true
optimize:
enabled: false

View file

@ -0,0 +1,88 @@
import { spawn } from 'child_process';
import { writeFileSync, readFile } from 'fs';
import { relative, resolve } from 'path';
import { safeDump } from 'js-yaml';
import es from 'event-stream';
import readYamlConfig from '../read_yaml_config';
import expect from 'expect.js';
const testConfigFile = follow(`fixtures/reload_logging_config/kibana.test.yml`);
const cli = follow(`../../../../bin/kibana`);
function follow(file) {
return relative(process.cwd(), resolve(__dirname, file));
}
function setLoggingJson(enabled) {
const conf = readYamlConfig(testConfigFile);
conf.logging = conf.logging || {};
conf.logging.json = enabled;
const yaml = safeDump(conf);
writeFileSync(testConfigFile, yaml);
return conf;
}
describe(`Server logging configuration`, function () {
it(`should be reloadable via SIGHUP process signaling`, function (done) {
let asserted = false;
let json = Infinity;
const conf = setLoggingJson(true);
const child = spawn(cli, [`--config`, testConfigFile]);
child.on('error', err => {
done(new Error(`error in child process while attempting to reload config.
${err.stack || err.message || err}`));
});
child.on('exit', code => {
expect(asserted).to.eql(true);
expect(code === null || code === 0).to.eql(true);
done();
});
child.stdout
.pipe(es.split())
.pipe(es.mapSync(function (line) {
if (!line) {
return line; // ignore empty lines
}
if (json--) {
expect(parseJsonLogLine).withArgs(line).to.not.throwError();
} else {
expectPlainTextLogLine(line);
}
}));
function parseJsonLogLine(line) {
try {
const data = JSON.parse(line);
const listening = data.tags.indexOf(`listening`) !== -1;
if (listening) {
switchToPlainTextLog();
}
} catch (err) {
expect(`Error parsing log line as JSON\n
${err.stack || err.message || err}`).to.eql(true);
}
}
function switchToPlainTextLog() {
json = 2; // ignore both "reloading" messages
setLoggingJson(false);
child.kill(`SIGHUP`); // reload logging config
}
function expectPlainTextLogLine(line) {
// assert
const tags = `[\u001b[32minfo\u001b[39m][\u001b[36mconfig\u001b[39m]`;
const status = `Reloaded logging configuration due to SIGHUP.`;
const expected = `${tags} ${status}`;
const actual = line.slice(-expected.length);
expect(actual).to.eql(expected);
// cleanup
asserted = true;
setLoggingJson(true);
child.kill();
}
});
});

View file

@ -2,11 +2,8 @@ import _ from 'lodash';
import { statSync } from 'fs';
import { isWorker } from 'cluster';
import { resolve } from 'path';
import readYamlConfig from './read_yaml_config';
import { fromRoot } from '../../utils';
const cwd = process.cwd();
import readYamlConfig from './read_yaml_config';
let canCluster;
try {
@ -28,7 +25,7 @@ const configPathCollector = pathCollector();
const pluginDirCollector = pathCollector();
const pluginPathCollector = pathCollector();
function initServerSettings(opts, extraCliOptions) {
function readServerSettings(opts, extraCliOptions) {
const settings = readYamlConfig(opts.config);
const set = _.partial(_.set, settings);
const get = _.partial(_.get, settings);
@ -128,7 +125,8 @@ module.exports = function (program) {
}
}
const settings = initServerSettings(opts, this.getUnknownOptions());
const getCurrentSettings = () => readServerSettings(opts, this.getUnknownOptions());
const settings = getCurrentSettings();
if (canCluster && opts.dev && !isWorker) {
// stop processing the action and handoff to cluster manager
@ -156,6 +154,13 @@ module.exports = function (program) {
process.exit(1); // eslint-disable-line no-process-exit
}
process.on('SIGHUP', function reloadConfig() {
const settings = getCurrentSettings();
kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.');
kbnServer.applyLoggingConfiguration(settings);
kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');
});
return kbnServer;
});
};

View file

@ -5,7 +5,14 @@ import mkdirp from 'mkdirp';
import Logger from '../../lib/logger';
import list from '../list';
import { join } from 'path';
import { writeFileSync } from 'fs';
import { writeFileSync, appendFileSync } from 'fs';
function createPlugin(name, version, pluginBaseDir) {
const pluginDir = join(pluginBaseDir, name);
mkdirp.sync(pluginDir);
appendFileSync(join(pluginDir, 'package.json'), '{"version": "' + version + '"}');
}
describe('kibana cli', function () {
@ -33,41 +40,61 @@ describe('kibana cli', function () {
});
it('list all of the folders in the plugin folder', function () {
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
createPlugin('plugin1', '5.0.0-alpha2', pluginDir);
createPlugin('plugin2', '3.2.1', pluginDir);
createPlugin('plugin3', '1.2.3', pluginDir);
list(settings, logger);
expect(logger.log.calledWith('plugin1')).to.be(true);
expect(logger.log.calledWith('plugin2')).to.be(true);
expect(logger.log.calledWith('plugin3')).to.be(true);
expect(logger.log.calledWith('plugin1@5.0.0-alpha2')).to.be(true);
expect(logger.log.calledWith('plugin2@3.2.1')).to.be(true);
expect(logger.log.calledWith('plugin3@1.2.3')).to.be(true);
});
it('ignore folders that start with a period', function () {
mkdirp.sync(join(pluginDir, '.foo'));
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
mkdirp.sync(join(pluginDir, '.bar'));
createPlugin('.foo', '1.0.0', pluginDir);
createPlugin('plugin1', '5.0.0-alpha2', pluginDir);
createPlugin('plugin2', '3.2.1', pluginDir);
createPlugin('plugin3', '1.2.3', pluginDir);
createPlugin('.bar', '1.0.0', pluginDir);
list(settings, logger);
expect(logger.log.calledWith('.foo')).to.be(false);
expect(logger.log.calledWith('.bar')).to.be(false);
expect(logger.log.calledWith('.foo@1.0.0')).to.be(false);
expect(logger.log.calledWith('.bar@1.0.0')).to.be(false);
});
it('list should only list folders', function () {
mkdirp.sync(join(pluginDir, 'plugin1'));
mkdirp.sync(join(pluginDir, 'plugin2'));
mkdirp.sync(join(pluginDir, 'plugin3'));
createPlugin('plugin1', '1.0.0', pluginDir);
createPlugin('plugin2', '1.0.0', pluginDir);
createPlugin('plugin3', '1.0.0', pluginDir);
writeFileSync(join(pluginDir, 'plugin4'), 'This is a file, and not a folder.');
list(settings, logger);
expect(logger.log.calledWith('plugin1')).to.be(true);
expect(logger.log.calledWith('plugin2')).to.be(true);
expect(logger.log.calledWith('plugin3')).to.be(true);
expect(logger.log.calledWith('plugin1@1.0.0')).to.be(true);
expect(logger.log.calledWith('plugin2@1.0.0')).to.be(true);
expect(logger.log.calledWith('plugin3@1.0.0')).to.be(true);
});
it('list should throw an exception if a plugin does not have a package.json', function () {
createPlugin('plugin1', '1.0.0', pluginDir);
mkdirp.sync(join(pluginDir, 'empty-plugin'));
expect(function () {
list(settings, logger);
}).to.throwError('Unable to read package.json file for plugin empty-plugin');
});
it('list should throw an exception if a plugin have an empty package.json', function () {
createPlugin('plugin1', '1.0.0', pluginDir);
const invalidPluginDir = join(pluginDir, 'invalid-plugin');
mkdirp.sync(invalidPluginDir);
appendFileSync(join(invalidPluginDir, 'package.json'), '');
expect(function () {
list(settings, logger);
}).to.throwError('Unable to read package.json file for plugin invalid-plugin');
});
});

View file

@ -1,4 +1,4 @@
import { statSync, readdirSync } from 'fs';
import { statSync, readdirSync, readFileSync } from 'fs';
import { join } from 'path';
export default function list(settings, logger) {
@ -7,7 +7,13 @@ export default function list(settings, logger) {
const stat = statSync(join(settings.pluginDir, filename));
if (stat.isDirectory() && filename[0] !== '.') {
logger.log(filename);
try {
const packagePath = join(settings.pluginDir, filename, 'package.json');
const { version } = JSON.parse(readFileSync(packagePath, 'utf8'));
logger.log(filename + '@' + version);
} catch (e) {
throw new Error('Unable to read package.json file for plugin ' + filename);
}
}
});
logger.log(''); //intentional blank line for aesthetics

View file

@ -2,6 +2,7 @@ import _ from 'lodash';
module.exports = {
'valueFormatter': _.identity,
'geohashGridAgg': { 'vis': { 'params': {} } },
'geoJson': {
'type': 'FeatureCollection',
'features': [

View file

@ -9,7 +9,7 @@ function VisDetailsSpyProvider(Notifier, $filter, $rootScope, config) {
template: visDebugSpyPanelTemplate,
order: 5,
link: function ($scope, $el) {
$scope.$watch('vis.getState() | json', function (json) {
$scope.$watch('vis.getEnabledState() | json', function (json) {
$scope.visStateJson = json;
});
}

View file

@ -27,6 +27,8 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
heatRadius: 25,
heatBlur: 15,
heatNormalizeData: true,
mapZoom: 2,
mapCenter: [15, 5],
wms: config.get('visualization:tileMap:WMSdefaults')
},
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'],
@ -46,54 +48,16 @@ export default function TileMapVisType(Private, getAppState, courier, config) {
pushFilter(filter, false, indexPatternName);
},
mapMoveEnd: function (event) {
const agg = _.get(event, 'chart.geohashGridAgg');
if (!agg) return;
agg.params.mapZoom = event.zoom;
agg.params.mapCenter = [event.center.lat, event.center.lng];
const editableVis = agg.vis.getEditableVis();
if (!editableVis) return;
const editableAgg = editableVis.aggs.byId[agg.id];
if (editableAgg) {
editableAgg.params.mapZoom = event.zoom;
editableAgg.params.mapCenter = [event.center.lat, event.center.lng];
}
mapMoveEnd: function (event, uiState) {
uiState.set('mapCenter', event.center);
},
mapZoomEnd: function (event) {
const agg = _.get(event, 'chart.geohashGridAgg');
if (!agg || !agg.params.autoPrecision) return;
mapZoomEnd: function (event, uiState) {
uiState.set('mapZoom', event.zoom);
// zoomPrecision maps event.zoom to a geohash precision value
// event.limit is the configurable max geohash precision
// default max precision is 7, configurable up to 12
const zoomPrecision = {
1: 2,
2: 2,
3: 2,
4: 3,
5: 3,
6: 4,
7: 4,
8: 5,
9: 5,
10: 6,
11: 6,
12: 7,
13: 7,
14: 8,
15: 9,
16: 10,
17: 11,
18: 12
};
const precision = config.get('visualization:tileMap:maxPrecision');
agg.params.precision = Math.min(zoomPrecision[event.zoom], precision);
courier.fetch();
const autoPrecision = _.get(event, 'chart.geohashGridAgg.params.autoPrecision');
if (autoPrecision) {
courier.fetch();
}
}
},
responseConverter: geoJsonConverter,

View file

@ -55,6 +55,10 @@ uiModules
// create child ui state from the savedObj
const uiState = panelConfig.uiState || {};
$scope.uiState = $scope.parentUiState.createChild(getPanelId(panelConfig.panel), uiState, true);
const panelSavedVis = _.get(panelConfig, 'savedObj.vis'); // Sometimes this will be a search, and undef
if (panelSavedVis) {
panelSavedVis.setUiState($scope.uiState);
}
$scope.filter = function (field, value, operator) {
const index = $scope.savedObj.searchSource.get('index').id;

View file

@ -496,7 +496,7 @@ app.controller('discover', function ($scope, config, courier, $route, $window, N
// we have a vis, just modify the aggs
if ($scope.vis) {
const visState = $scope.vis.getState();
const visState = $scope.vis.getEnabledState();
visState.aggs = visStateAggs;
$scope.vis.setState(visState);

View file

@ -0,0 +1,13 @@
import _ from 'lodash';
import $ from 'jquery';
import uiModules from 'ui/modules';
import noResultsTemplate from '../partials/no_results.html';
uiModules
.get('apps/discover')
.directive('discoverNoResults', function () {
return {
restrict: 'E',
template: noResultsTemplate
};
});

View file

@ -56,58 +56,7 @@
<div class="discover-wrapper col-md-10">
<div class="discover-content">
<!-- no results -->
<div ng-show="resultState === 'none'">
<div class="col-md-10 col-md-offset-1">
<h1>No results found <i aria-hidden="true" class="fa fa-meh-o"></i></h1>
<p>
Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas:
</p>
<div class="shard-failures" ng-show="failures">
<h3>Shard Failures</h3>
<p>The following shard failures ocurred:</p>
<ul>
<li ng-repeat="failure in failures | limitTo: failuresShown"><strong>Index:</strong> {{failure.index}} <strong>Shard:</strong> {{failure.shard}} <strong>Reason:</strong> {{failure.reason}} </li>
</ul>
<a ng-click="showAllFailures()" ng-if="failures.length > failuresShown" title="Show More">Show More</a>
<a ng-click="showLessFailures()" ng-if="failures.length === failuresShown && failures.length > 5" title="Show Less">Show Less</a>
</div>
<div ng-show="opts.timefield">
<p>
<h3>Expand your time range</h3>
<p>I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Try selecting a wider time range by opening the time picker <i aria-hidden="true" class="fa fa-clock-o"></i> in the top right corner of your screen.
</p>
</div>
<h3>Refine your query</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
</p>
<p>
<h4>Examples:</h4>
Find requests that contain the number 200, in any field:
<pre>200</pre>
Or we can search in a specific field. Find 200 in the status field:
<pre>status:200</pre>
Find all status codes between 400-499:
<pre>status:[400 TO 499]</pre>
Find status codes 400-499 with the extension php:
<pre>status:[400 TO 499] AND extension:PHP</pre>
Or HTML
<pre>status:[400 TO 499] AND (extension:php OR extension:html)</pre>
</p>
</div>
</div>
<discover-no-results ng-show="resultState === 'none'"></discover-no-results>
<!-- loading -->
<div ng-show="resultState === 'loading'">

View file

@ -1,4 +1,5 @@
import 'plugins/kibana/discover/saved_searches/saved_searches';
import 'plugins/kibana/discover/directives/no_results';
import 'plugins/kibana/discover/directives/timechart';
import 'ui/navbar_extensions';
import 'ui/collapsible_sidebar';

View file

@ -0,0 +1,51 @@
<div>
<div class="col-md-10 col-md-offset-1" data-test-subj="discoverNoResults">
<h1>No results found <i aria-hidden="true" class="fa fa-meh-o"></i></h1>
<p>
Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas:
</p>
<div class="shard-failures" ng-show="failures">
<h3>Shard Failures</h3>
<p>The following shard failures ocurred:</p>
<ul>
<li ng-repeat="failure in failures | limitTo: failuresShown"><strong>Index:</strong> {{failure.index}} <strong>Shard:</strong> {{failure.shard}} <strong>Reason:</strong> {{failure.reason}} </li>
</ul>
<a ng-click="showAllFailures()" ng-if="failures.length > failuresShown" title="Show More">Show More</a>
<a ng-click="showLessFailures()" ng-if="failures.length === failuresShown && failures.length > 5" title="Show Less">Show Less</a>
</div>
<div ng-show="opts.timefield">
<p>
<h3>Expand your time range</h3>
<p>I see you are looking at an index with a date field. It is possible your query does not match anything in the current time range, or that there is no data at all in the currently selected time range. Click the button below to open the time picker. For future reference you can open the time picker by clicking on the <a class="btn btn-xs navbtn" ng-click="kbnTopNav.toggle('filter')" aria-expanded="kbnTopNav.is('filter')" aria-label="time picker" data-test-subj="discoverNoResultsTimefilter"><i aria-hidden="true" class="fa fa-clock-o"></i> time picker</a> button in the top right corner of your screen.
</p>
</div>
<h3>Refine your query</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
</p>
<p>
<h4>Examples:</h4>
Find requests that contain the number 200, in any field:
<pre>200</pre>
Or we can search in a specific field. Find 200 in the status field:
<pre>status:200</pre>
Find all status codes between 400-499:
<pre>status:[400 TO 499]</pre>
Find status codes 400-499 with the extension php:
<pre>status:[400 TO 499] AND extension:PHP</pre>
Or HTML
<pre>status:[400 TO 499] AND (extension:php OR extension:html)</pre>
</p>
</div>
</div>

View file

@ -83,9 +83,12 @@ uiModules.get('apps/settings')
};
$scope.bulkDelete = function () {
$scope.currentTab.service.delete(pluck($scope.selectedItems, 'id')).then(refreshData).then(function () {
$scope.currentTab.service.delete(pluck($scope.selectedItems, 'id'))
.then(refreshData)
.then(function () {
$scope.selectedItems.length = 0;
});
})
.catch(error => notify.error(error));
};
$scope.bulkExport = function () {

View file

@ -204,3 +204,5 @@ kbn-settings-indices {
.kbn-settings-indices-create {
.time-and-pattern > div {}
}
@import "~ui/dragula/gu-dragula.less";

View file

@ -0,0 +1,121 @@
import angular from 'angular';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
let init;
let $rootScope;
let $compile;
describe('draggable_* directives', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
init = function init(markup = '') {
const $parentScope = $rootScope.$new();
$parentScope.items = [
{ name: 'item_1' },
{ name: 'item_2' },
{ name: 'item_3' }
];
// create the markup
const $elem = angular.element(`<div draggable-container="items">`);
$elem.html(markup);
// compile the directive
$compile($elem)($parentScope);
$parentScope.$apply();
const $scope = $elem.scope();
return { $parentScope, $scope, $elem };
};
}));
describe('draggable_container directive', function () {
it('should expose the drake', function () {
const { $scope } = init();
expect($scope.drake).to.be.an(Object);
});
it('should expose the controller', function () {
const { $scope } = init();
expect($scope.draggableContainerCtrl).to.be.an(Object);
});
it('should pull item list from directive attribute', function () {
const { $scope, $parentScope } = init();
expect($scope.draggableContainerCtrl.getList()).to.eql($parentScope.items);
});
it('should not be able to move extraneous DOM elements', function () {
const bare = angular.element(`<div>`);
const { $scope } = init();
expect($scope.drake.canMove(bare[0])).to.eql(false);
});
it('should not be able to move non-[draggable-item] elements', function () {
const bare = angular.element(`<div>`);
const { $scope, $elem } = init();
$elem.append(bare);
expect($scope.drake.canMove(bare[0])).to.eql(false);
});
it('shouldn\'t be able to move extraneous [draggable-item] elements', function () {
const anotherParent = angular.element(`<div draggable-container="items">`);
const item = angular.element(`<div draggable-item="items[0]">`);
const scope = $rootScope.$new();
anotherParent.append(item);
$compile(anotherParent)(scope);
$compile(item)(scope);
scope.$apply();
const { $scope } = init();
expect($scope.drake.canMove(item[0])).to.eql(false);
});
it('shouldn\'t be able to move [draggable-item] if it has a handle', function () {
const { $scope, $elem } = init(`
<div draggable-item="items[0]">
<div draggable-handle></div>
</div>
`);
const item = $elem.find(`[draggable-item]`);
expect($scope.drake.canMove(item[0])).to.eql(false);
});
it('should be able to move [draggable-item] by its handle', function () {
const { $scope, $elem } = init(`
<div draggable-item="items[0]">
<div draggable-handle></div>
</div>
`);
const handle = $elem.find(`[draggable-handle]`);
expect($scope.drake.canMove(handle[0])).to.eql(true);
});
});
describe('draggable_item', function () {
it('should be required to be a child to [draggable-container]', function () {
const item = angular.element(`<div draggable-item="items[0]">`);
const scope = $rootScope.$new();
expect(() => {
$compile(item)(scope);
scope.$apply();
}).to.throwException(/controller(.+)draggableContainer(.+)required/i);
});
});
describe('draggable_handle', function () {
it('should be required to be a child to [draggable-item]', function () {
const handle = angular.element(`<div draggable-handle>`);
const scope = $rootScope.$new();
expect(() => {
$compile(handle)(scope);
scope.$apply();
}).to.throwException(/controller(.+)draggableItem(.+)required/i);
});
});
});

View file

@ -27,30 +27,40 @@
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<!-- disable aggregation -->
<button
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
ng-if="agg.enabled && canRemove(agg)"
ng-click="agg.enabled = false"
aria-label="Disable aggregation"
tooltip="Disable aggregation"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-primary">
<i aria-hidden="true" class="fa fa-caret-up"></i>
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-toggle-on"></i>
</button>
<!-- down button -->
<!-- enable aggregation -->
<button
aria-label="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
ng-if="!agg.enabled"
ng-click="agg.enabled = true"
aria-label="Enable aggregation"
tooltip="Enable aggregation"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-primary">
<i aria-hidden="true" class="fa fa-caret-down"></i>
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-toggle-off"></i>
</button>
<!-- drag handle -->
<button
draggable-handle
aria-label="Modify Priority by Dragging"
ng-if="stats.count > 1"
tooltip="Modify Priority by Dragging"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-arrows-v"></i>
</button>
<!-- remove button -->
@ -79,5 +89,6 @@
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
ng-hide="dragging"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>

View file

@ -21,7 +21,6 @@ uiModules
template: aggTemplate,
require: 'form',
link: function ($scope, $el, attrs, kbnForm) {
$scope.$bind('outputAgg', 'outputVis.aggs.byId[agg.id]', $scope);
$scope.editorOpen = !!$scope.agg.brandNew;
$scope.$watch('editorOpen', function (open) {
@ -47,13 +46,16 @@ uiModules
return label ? label : '';
};
function move(below, agg) {
_.move($scope.vis.aggs, agg, below, function (otherAgg) {
return otherAgg.schema.group === agg.schema.group;
});
}
$scope.moveUp = _.partial(move, false);
$scope.moveDown = _.partial(move, true);
$scope.$on('drag-start', e => {
$scope.editorWasOpen = $scope.editorOpen;
$scope.editorOpen = false;
$scope.$emit('agg-drag-start', $scope.agg);
});
$scope.$on('drag-end', e => {
$scope.editorOpen = $scope.editorWasOpen;
$scope.$emit('agg-drag-end', $scope.agg);
});
$scope.remove = function (agg) {
const aggs = $scope.vis.aggs;

View file

@ -3,9 +3,9 @@
{{ groupName }}
</div>
<div class="vis-editor-agg-group" ng-class="groupName">
<div ng-class="groupName" draggable-container="vis.aggs" class="vis-editor-agg-group">
<!-- wrapper needed for nesting-indicator -->
<div ng-repeat="agg in group" class="vis-editor-agg-wrapper">
<div ng-repeat="agg in group" draggable-item="agg" class="vis-editor-agg-wrapper">
<!-- agg.html - controls for aggregation -->
<ng-form vis-editor-agg name="aggForm" class="vis-editor-agg"></ng-form>
</div>

View file

@ -41,6 +41,9 @@ uiModules
if (count < schema.max) return true;
});
});
$scope.$on('agg-drag-start', e => $scope.dragging = true);
$scope.$on('agg-drag-end', e => $scope.dragging = false);
}
};

View file

@ -0,0 +1,88 @@
import _ from 'lodash';
import $ from 'jquery';
import dragula from 'dragula';
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableContainer', function () {
return {
restrict: 'A',
scope: true,
controllerAs: 'draggableContainerCtrl',
controller($scope, $attrs, $parse) {
this.getList = () => $parse($attrs.draggableContainer)($scope);
},
link($scope, $el, attr) {
const drake = dragula({
containers: $el.toArray(),
moves(el, source, handle) {
const itemScope = $(el).scope();
if (!('draggableItemCtrl' in itemScope)) {
return; // only [draggable-item] is draggable
}
return itemScope.draggableItemCtrl.moves(handle);
}
});
const drakeEvents = [
'cancel',
'cloned',
'drag',
'dragend',
'drop',
'out',
'over',
'remove',
'shadow'
];
const prettifiedDrakeEvents = {
drag: 'start',
dragend: 'end'
};
drakeEvents.forEach(type => {
drake.on(type, (el, ...args) => forwardEvent(type, el, ...args));
});
drake.on('drag', markDragging(true));
drake.on('dragend', markDragging(false));
drake.on('drop', drop);
$scope.$on('$destroy', drake.destroy);
$scope.drake = drake;
function markDragging(isDragging) {
return el => {
const scope = $(el).scope();
scope.isDragging = isDragging;
scope.$apply();
};
}
function forwardEvent(type, el, ...args) {
const name = `drag-${prettifiedDrakeEvents[type] || type}`;
const scope = $(el).scope();
scope.$broadcast(name, el, ...args);
}
function drop(el, target, source, sibling) {
const list = $scope.draggableContainerCtrl.getList();
const itemScope = $(el).scope();
const item = itemScope.draggableItemCtrl.getItem();
const toIndex = getSiblingItemIndex(list, sibling);
_.move(list, item, toIndex);
}
function getSiblingItemIndex(list, sibling) {
if (!sibling) { // means the item was dropped at the end of the list
return list.length - 1;
}
const siblingScope = $(sibling).scope();
const siblingItem = siblingScope.draggableItemCtrl.getItem();
const siblingIndex = list.indexOf(siblingItem);
return siblingIndex;
}
}
};
});

View file

@ -0,0 +1,14 @@
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableHandle', function () {
return {
restrict: 'A',
require: '^draggableItem',
link($scope, $el, attr, ctrl) {
ctrl.registerHandle($el);
$el.addClass('gu-handle');
}
};
});

View file

@ -0,0 +1,29 @@
import $ from 'jquery';
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableItem', function () {
return {
restrict: 'A',
require: '^draggableContainer',
scope: true,
controllerAs: 'draggableItemCtrl',
controller($scope, $attrs, $parse) {
const dragHandles = $();
this.getItem = () => $parse($attrs.draggableItem)($scope);
this.registerHandle = $el => {
dragHandles.push(...$el);
};
this.moves = handle => {
const $handle = $(handle);
const $anywhereInParentChain = $handle.parents().addBack();
const movable = dragHandles.is($anywhereInParentChain);
return movable;
};
},
link($scope, $el, attr) {
}
};
});

View file

@ -119,8 +119,8 @@ uiModules
if (!angular.equals($state.vis, savedVisState)) {
Promise.try(function () {
vis.setState($state.vis);
editableVis.setState($state.vis);
vis.setState(editableVis.getEnabledState());
})
.catch(courier.redirectWhenMissing({
'index-pattern-field': '/visualize'
@ -139,6 +139,7 @@ uiModules
$scope.editableVis = editableVis;
$scope.state = $state;
$scope.uiState = $state.makeStateful('uiState');
vis.setUiState($scope.uiState);
$scope.timefilter = timefilter;
$scope.opts = _.pick($scope, 'doSave', 'savedVis', 'shareData', 'timefilter');
@ -149,9 +150,9 @@ uiModules
$scope.stageEditableVis = transferVisState(editableVis, vis, true);
$scope.resetEditableVis = transferVisState(vis, editableVis);
$scope.$watch(function () {
return editableVis.getState();
return editableVis.getEnabledState();
}, function (newState) {
editableVis.dirty = !angular.equals(newState, vis.getState());
editableVis.dirty = !angular.equals(newState, vis.getEnabledState());
$scope.responseValueAggs = null;
try {
@ -291,14 +292,16 @@ uiModules
}
};
function transferVisState(fromVis, toVis, fetch) {
function transferVisState(fromVis, toVis, stage) {
return function () {
toVis.setState(fromVis.getState());
const view = fromVis.getEnabledState();
const full = fromVis.getState();
toVis.setState(view);
editableVis.dirty = false;
$state.vis = vis.getState();
$state.vis = full;
$state.save();
if (fetch) $scope.fetch();
if (stage) $scope.fetch();
};
}

View file

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

View file

@ -11,6 +11,9 @@ import 'plugins/kibana/visualize/editor/agg_params';
import 'plugins/kibana/visualize/editor/nesting_indicator';
import 'plugins/kibana/visualize/editor/sidebar';
import 'plugins/kibana/visualize/editor/vis_options';
import 'plugins/kibana/visualize/editor/draggable_container';
import 'plugins/kibana/visualize/editor/draggable_item';
import 'plugins/kibana/visualize/editor/draggable_handle';
import 'plugins/kibana/visualize/saved_visualizations/_saved_vis';
import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations';
import uiRoutes from 'ui/routes';

View file

@ -25,14 +25,16 @@
No plugin status information available
</h4>
<table class="plugin_status_breakdown row" ng-if="ui.statuses">
<tr>
<th class="col-xs-1">Name</th>
<th class="col-xs-11">Status</th>
<table class="plugin_status_breakdown" ng-if="ui.statuses">
<tr class="row">
<th class="col-xs-2">Name</th>
<th class="col-xs-2">Version</th>
<th class="col-xs-8">Status</th>
</tr>
<tr ng-repeat="status in ui.statuses" class="status_row plugin_state_default plugin_state_{{status.state}}">
<td class="col-xs-1 status_name">{{status.name}}</td>
<td class="col-xs-11 status_message">
<tr ng-repeat="status in ui.statuses" class="status_row plugin_state_default plugin_state_{{status.state}} row">
<td class="col-xs-2 status_name">{{status.name}}</td>
<td class="col-xs-2 status_version">{{status.version}}</td>
<td class="col-xs-8 status_message">
<i class="fa plugin_state_color plugin_state_icon" />
{{status.message}}
</td>

View file

@ -3,6 +3,8 @@ import { constant, once, compact, flatten } from 'lodash';
import { promisify, resolve, fromNode } from 'bluebird';
import { isWorker } from 'cluster';
import { fromRoot, pkg } from '../utils';
import Config from './config/config';
import loggingConfiguration from './logging/configuration';
let rootDir = fromRoot('.');
@ -107,4 +109,16 @@ module.exports = class KbnServer {
}
});
}
applyLoggingConfiguration(settings) {
const config = Config.withDefaultSchema(settings);
const loggingOptions = loggingConfiguration(config);
const subset = {
ops: config.get('ops'),
logging: config.get('logging')
};
const plain = JSON.stringify(subset, null, 2);
this.server.log(['info', 'config'], 'New logging configuration:\n' + plain);
this.server.plugins['even-better'].monitor.reconfigure(loggingOptions);
}
};

View file

@ -0,0 +1,61 @@
import _ from 'lodash';
import logReporter from './log_reporter';
export default function loggingConfiguration(config) {
let events = config.get('logging.events');
if (config.get('logging.silent')) {
_.defaults(events, {});
}
else if (config.get('logging.quiet')) {
_.defaults(events, {
log: ['listening', 'error', 'fatal'],
request: ['error'],
error: '*'
});
}
else if (config.get('logging.verbose')) {
_.defaults(events, {
log: '*',
ops: '*',
request: '*',
response: '*',
error: '*'
});
}
else {
_.defaults(events, {
log: ['info', 'warning', 'error', 'fatal'],
response: config.get('logging.json') ? '*' : '!',
request: ['info', 'warning', 'error', 'fatal'],
error: '*'
});
}
const options = {
opsInterval: config.get('ops.interval'),
requestHeaders: true,
requestPayload: true,
reporters: [
{
reporter: logReporter,
config: {
json: config.get('logging.json'),
dest: config.get('logging.dest'),
// I'm adding the default here because if you add another filter
// using the commandline it will remove authorization. I want users
// to have to explicitly set --logging.filter.authorization=none to
// have it show up int he logs.
filter: _.defaults(config.get('logging.filter'), {
authorization: 'remove'
})
},
events: _.transform(events, function (filtered, val, key) {
// provide a string compatible way to remove events
if (val !== '!') filtered[key] = val;
}, {})
}
]
};
return options;
}

View file

@ -1,68 +1,15 @@
import _ from 'lodash';
import { fromNode } from 'bluebird';
import evenBetter from 'even-better';
import loggingConfiguration from './configuration';
module.exports = function (kbnServer, server, config) {
export default function (kbnServer, server, config) {
// prevent relying on kbnServer so this can be used with other hapi servers
kbnServer = null;
return fromNode(function (cb) {
let events = config.get('logging.events');
if (config.get('logging.silent')) {
_.defaults(events, {});
}
else if (config.get('logging.quiet')) {
_.defaults(events, {
log: ['listening', 'error', 'fatal'],
request: ['error'],
error: '*'
});
}
else if (config.get('logging.verbose')) {
_.defaults(events, {
log: '*',
ops: '*',
request: '*',
response: '*',
error: '*'
});
}
else {
_.defaults(events, {
log: ['info', 'warning', 'error', 'fatal'],
response: config.get('logging.json') ? '*' : '!',
request: ['info', 'warning', 'error', 'fatal'],
error: '*'
});
}
server.register({
register: require('good'),
options: {
opsInterval: config.get('ops.interval'),
requestHeaders: true,
requestPayload: true,
reporters: [
{
reporter: require('./log_reporter'),
config: {
json: config.get('logging.json'),
dest: config.get('logging.dest'),
// I'm adding the default here because if you add another filter
// using the commandline it will remove authorization. I want users
// to have to explicitly set --logging.filter.authorization=none to
// have it show up int he logs.
filter: _.defaults(config.get('logging.filter'), {
authorization: 'remove'
})
},
events: _.transform(events, function (filtered, val, key) {
// provide a string compatible way to remove events
if (val !== '!') filtered[key] = val;
}, {})
}
]
}
register: evenBetter,
options: loggingConfiguration(config)
}, cb);
});
};

View file

@ -19,6 +19,7 @@ let typeColors = {
req: 'green',
res: 'green',
ops: 'cyan',
config: 'cyan',
err: 'red',
info: 'green',
error: 'red',

View file

@ -119,7 +119,7 @@ module.exports = class Plugin {
}));
server.log(['plugins', 'debug'], {
tmpl: 'Initializing plugin <%= plugin.id %>',
tmpl: 'Initializing plugin <%= plugin.toString() %>',
plugin: this
});
@ -127,7 +127,7 @@ module.exports = class Plugin {
server.exposeStaticDir(`/plugins/${id}/{path*}`, this.publicDir);
}
this.status = kbnServer.status.create(`plugin:${this.id}`);
this.status = kbnServer.status.create(this);
server.expose('status', this.status);
return await attempt(this.externalInit, [server, options], this);

View file

@ -7,6 +7,8 @@ import Status from '../status';
import ServerStatus from '../server_status';
describe('ServerStatus class', function () {
const plugin = {id: 'name', version: '1.2.3'};
let server;
let serverStatus;
@ -15,23 +17,23 @@ describe('ServerStatus class', function () {
serverStatus = new ServerStatus(server);
});
describe('#create(name)', function () {
it('should create a new status by name', function () {
let status = serverStatus.create('name');
describe('#create(plugin)', function () {
it('should create a new status by plugin', function () {
let status = serverStatus.create(plugin);
expect(status).to.be.a(Status);
});
});
describe('#get(name)', function () {
it('exposes plugins by name', function () {
let status = serverStatus.create('name');
it('exposes plugins by its id/name', function () {
let status = serverStatus.create(plugin);
expect(serverStatus.get('name')).to.be(status);
});
});
describe('#getState(name)', function () {
it('should expose the state of the plugin by name', function () {
let status = serverStatus.create('name');
let status = serverStatus.create(plugin);
status.green();
expect(serverStatus.getState('name')).to.be('green');
});
@ -39,7 +41,7 @@ describe('ServerStatus class', function () {
describe('#overall()', function () {
it('considers each status to produce a summary', function () {
let status = serverStatus.create('name');
let status = serverStatus.create(plugin);
expect(serverStatus.overall().state).to.be('uninitialized');
@ -65,9 +67,13 @@ describe('ServerStatus class', function () {
describe('#toJSON()', function () {
it('serializes to overall status and individuals', function () {
let one = serverStatus.create('one');
let two = serverStatus.create('two');
let three = serverStatus.create('three');
const pluginOne = {id: 'one', version: '1.0.0'};
const pluginTwo = {id: 'two', version: '2.0.0'};
const pluginThree = {id: 'three', version: '3.0.0'};
let one = serverStatus.create(pluginOne);
let two = serverStatus.create(pluginTwo);
let three = serverStatus.create(pluginThree);
one.green();
two.yellow();

View file

@ -4,6 +4,8 @@ import Status from '../status';
import ServerStatus from '../server_status';
describe('Status class', function () {
const plugin = {id: 'test', version: '1.2.3'};
let server;
let serverStatus;
@ -13,11 +15,11 @@ describe('Status class', function () {
});
it('should have an "uninitialized" state initially', function () {
expect(serverStatus.create('test')).to.have.property('state', 'uninitialized');
expect(serverStatus.create(plugin)).to.have.property('state', 'uninitialized');
});
it('emits change when the status is set', function (done) {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
status.once('change', function (prev, prevMsg) {
expect(status.state).to.be('green');
@ -40,7 +42,7 @@ describe('Status class', function () {
});
it('should only trigger the change listener when something changes', function () {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let stub = sinon.stub();
status.on('change', stub);
status.green('Ready');
@ -50,16 +52,18 @@ describe('Status class', function () {
});
it('should create a JSON representation of the status', function () {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
status.green('Ready');
let json = status.toJSON();
expect(json.name).to.eql(plugin.id);
expect(json.version).to.eql(plugin.version);
expect(json.state).to.eql('green');
expect(json.message).to.eql('Ready');
});
it('should call on handler if status is already matched', function (done) {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let msg = 'Test Ready';
status.green(msg);
@ -73,7 +77,7 @@ describe('Status class', function () {
});
it('should call once handler if status is already matched', function (done) {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let msg = 'Test Ready';
status.green(msg);
@ -88,7 +92,7 @@ describe('Status class', function () {
function testState(color) {
it(`should change the state to ${color} when #${color}() is called`, function () {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let message = 'testing ' + color;
status[color](message);
expect(status).to.have.property('state', color);
@ -96,7 +100,7 @@ describe('Status class', function () {
});
it(`should trigger the "change" listner when #${color}() is called`, function (done) {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let message = 'testing ' + color;
status.on('change', function (prev, prevMsg) {
expect(status.state).to.be(color);
@ -110,7 +114,7 @@ describe('Status class', function () {
});
it(`should trigger the "${color}" listner when #${color}() is called`, function (done) {
let status = serverStatus.create('test');
let status = serverStatus.create(plugin);
let message = 'testing ' + color;
status.on(color, function (prev, prevMsg) {
expect(status.state).to.be(color);

View file

@ -6,7 +6,7 @@ import { join } from 'path';
export default function (kbnServer, server, config) {
kbnServer.status = new ServerStatus(kbnServer.server);
if (server.plugins.good) {
if (server.plugins['even-better']) {
kbnServer.mixin(require('./metrics'));
}

View file

@ -5,7 +5,7 @@ module.exports = function (kbnServer, server, config) {
kbnServer.metrics = new Samples(12);
server.plugins.good.monitor.on('ops', function (event) {
server.plugins['even-better'].monitor.on('ops', function (event) {
let now = Date.now();
let secSinceLast = (now - lastReport) / 1000;
lastReport = now;

View file

@ -9,8 +9,8 @@ module.exports = class ServerStatus {
this._created = {};
}
create(name) {
return (this._created[name] = new Status(name, this.server));
create(plugin) {
return (this._created[plugin.id] = new Status(plugin, this.server));
}
each(fn) {

View file

@ -3,22 +3,21 @@ import states from './states';
import { EventEmitter } from 'events';
class Status extends EventEmitter {
constructor(name, server) {
constructor(plugin, server) {
super();
this.name = name;
this.plugin = plugin;
this.since = new Date();
this.state = 'uninitialized';
this.message = 'uninitialized';
this.on('change', function (previous, previousMsg) {
this.since = new Date();
let tags = ['status', name];
let tags = ['status', `plugin:${this.plugin.toString()}`];
tags.push(this.state === 'red' ? 'error' : 'info');
server.log(tags, {
tmpl: 'Status changed from <%= prevState %> to <%= state %><%= message ? " - " + message : "" %>',
name: name,
state: this.state,
message: this.message,
prevState: previous,
@ -29,7 +28,8 @@ class Status extends EventEmitter {
toJSON() {
return {
name: this.name,
name: this.plugin.id,
version: this.plugin.version,
state: this.state,
icon: states.get(this.state).icon,
message: this.message,

View file

@ -34,8 +34,8 @@ export default function TileMapConverterFn(Private, timefilter, $compile, $rootS
properties: {
min: _.min(values),
max: _.max(values),
zoom: _.get(geoAgg, 'params.mapZoom'),
center: _.get(geoAgg, 'params.mapCenter')
zoom: geoAgg && geoAgg.vis.uiStateVal('mapZoom'),
center: geoAgg && geoAgg.vis.uiStateVal('mapCenter')
}
}
};

View file

@ -6,6 +6,30 @@ export default function GeoHashAggDefinition(Private, config) {
let BucketAggType = Private(AggTypesBucketsBucketAggTypeProvider);
let defaultPrecision = 2;
// zoomPrecision maps event.zoom to a geohash precision value
// event.limit is the configurable max geohash precision
// default max precision is 7, configurable up to 12
const zoomPrecision = {
1: 2,
2: 2,
3: 2,
4: 3,
5: 3,
6: 4,
7: 4,
8: 5,
9: 5,
10: 6,
11: 6,
12: 7,
13: 7,
14: 8,
15: 9,
16: 10,
17: 11,
18: 12
};
function getPrecision(precision) {
let maxPrecision = _.parseInt(config.get('visualization:tileMap:maxPrecision'));
@ -45,19 +69,15 @@ export default function GeoHashAggDefinition(Private, config) {
},
{
name: 'precision',
default: defaultPrecision,
editor: precisionTemplate,
controller: function ($scope) {
$scope.$watchMulti([
'agg.params.autoPrecision',
'outputAgg.params.precision'
], function (cur, prev) {
if (cur[1]) $scope.agg.params.precision = cur[1];
});
},
deserialize: getPrecision,
controller: function ($scope) {
},
write: function (aggConfig, output) {
output.params.precision = getPrecision(aggConfig.params.precision);
const vis = aggConfig.vis;
const currZoom = vis.hasUiState() && vis.uiStateVal('mapZoom');
const autoPrecisionVal = zoomPrecision[(currZoom || vis.params.mapZoom)];
output.params.precision = aggConfig.params.autoPrecision ? autoPrecisionVal : getPrecision(aggConfig.params.precision);
}
}
]

View file

@ -0,0 +1,13 @@
.gu-handle {
cursor: move;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
}
.gu-mirror,
.gu-mirror .gu-handle {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}

View file

@ -607,3 +607,5 @@ fieldset {
}
}
}
@import (reference) "~dragula/dist/dragula.css";

View file

@ -29,7 +29,7 @@ paginate {
a {
text-decoration: none;
background-color: @white;
background-color: @kibanaGray6;
margin-left: 2px;
padding: 8px 11px;
}
@ -42,6 +42,7 @@ paginate {
text-decoration: none !important;
font-weight: bold;
color: @paginate-page-link-active-color;
cursor: default;
}
}
}

View file

@ -36,6 +36,7 @@
font-size: 1.5em;
padding: 5px 0 6px 0;
margin: 0 10px;
border-bottom: 2px solid transparent;
}
// Active, hover state for the getTabs
> .active > a,

View file

@ -58,7 +58,7 @@ describe('Vis Class', function () {
describe('getState()', function () {
it('should get a state that represents the... er... state', function () {
let state = vis.getState();
let state = vis.getEnabledState();
expect(state).to.have.property('type', 'pie');
expect(state).to.have.property('params');

View file

@ -9,6 +9,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
self.id = String(opts.id || AggConfig.nextId(vis.aggs));
self.vis = vis;
self._opts = opts = (opts || {});
self.enabled = typeof opts.enabled === 'boolean' ? opts.enabled : true;
// setters
self.type = opts.type;
@ -232,6 +233,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
return {
id: self.id,
enabled: self.enabled,
type: self.type && self.type.name,
schema: self.schema && self.schema.name,
params: outParams

View file

@ -2,16 +2,18 @@ import _ from 'lodash';
import AggTypesIndexProvider from 'ui/agg_types/index';
import RegistryVisTypesProvider from 'ui/registry/vis_types';
import VisAggConfigsProvider from 'ui/vis/agg_configs';
import PersistedStateProvider from 'ui/persisted_state/persisted_state';
export default function VisFactory(Notifier, Private) {
let aggTypes = Private(AggTypesIndexProvider);
let visTypes = Private(RegistryVisTypesProvider);
let AggConfigs = Private(VisAggConfigsProvider);
const PersistedState = Private(PersistedStateProvider);
let notify = new Notifier({
location: 'Vis'
});
function Vis(indexPattern, state) {
function Vis(indexPattern, state, uiState) {
state = state || {};
if (_.isString(state)) {
@ -24,6 +26,7 @@ export default function VisFactory(Notifier, Private) {
// http://aphyr.com/data/posts/317/state.gif
this.setState(state);
this.setUiState(uiState);
}
Vis.convertOldState = function (type, oldState) {
@ -44,7 +47,7 @@ export default function VisFactory(Notifier, Private) {
oldConfigs.forEach(function (oldConfig) {
let agg = {
schema: schema.name,
type: oldConfig.agg,
type: oldConfig.agg
};
let aggType = aggTypes.byName[agg.type];
@ -81,18 +84,27 @@ export default function VisFactory(Notifier, Private) {
this.aggs = new AggConfigs(this, state.aggs);
};
Vis.prototype.getState = function () {
Vis.prototype.getStateInternal = function (includeDisabled) {
return {
title: this.title,
type: this.type.name,
params: this.params,
aggs: this.aggs.map(function (agg) {
return agg.toJSON();
}).filter(Boolean),
aggs: this.aggs
.filter(agg => includeDisabled || agg.enabled)
.map(agg => agg.toJSON())
.filter(Boolean),
listeners: this.listeners
};
};
Vis.prototype.getEnabledState = function () {
return this.getStateInternal(false);
};
Vis.prototype.getState = function () {
return this.getStateInternal(true);
};
Vis.prototype.createEditableVis = function () {
return this._editableVis || (this._editableVis = this.clone());
};
@ -102,7 +114,8 @@ export default function VisFactory(Notifier, Private) {
};
Vis.prototype.clone = function () {
return new Vis(this.indexPattern, this.getState());
const uiJson = this.hasUiState() ? this.getUiState().toJSON() : {};
return new Vis(this.indexPattern, this.getState(), uiJson);
};
Vis.prototype.requesting = function () {
@ -125,5 +138,26 @@ export default function VisFactory(Notifier, Private) {
});
};
Vis.prototype.hasUiState = function () {
return !!this.__uiState;
};
Vis.prototype.setUiState = function (uiState) {
if (uiState instanceof PersistedState) {
this.__uiState = uiState;
}
};
Vis.prototype.getUiState = function () {
return this.__uiState;
};
Vis.prototype.uiStateVal = function (key, val) {
if (this.hasUiState()) {
if (_.isUndefined(val)) {
return this.__uiState.get(key);
}
return this.__uiState.set(key, val);
}
return val;
};
return Vis;
};

View file

@ -81,6 +81,7 @@ describe('TileMap Tests', function () {
it('should only add controls if data exists', function () {
let noData = {
geohashGridAgg: { vis: { params: {} } },
geoJson: {
features: [],
properties: {},

View file

@ -55,7 +55,7 @@ export default function HandlerBaseClass(Private) {
this.getProxyHandler = _.memoize(function (event) {
let self = this;
return function (e) {
self.vis.emit(event, e);
self.vis.emit(event, e, vis.uiState);
};
});
}

View file

@ -47,6 +47,8 @@ export default function MapFactory(Private) {
this._valueFormatter = params.valueFormatter || _.identity;
this._tooltipFormatter = params.tooltipFormatter || _.identity;
this._geoJson = _.get(this._chartData, 'geoJson');
this._mapZoom = params.zoom || defaultMapZoom;
this._mapCenter = params.center || defaultMapCenter;
this._attr = params.attr || {};
let mapOptions = {
@ -211,7 +213,8 @@ export default function MapFactory(Private) {
this.map.on('moveend', function setZoomCenter(ev) {
if (!self.map) return;
// update internal center and zoom references
self._mapCenter = self.map.getCenter();
const uglyCenter = self.map.getCenter();
self._mapCenter = [uglyCenter.lat, uglyCenter.lng];
self._mapZoom = self.map.getZoom();
self._addMarkers();
@ -272,10 +275,6 @@ export default function MapFactory(Private) {
TileMapMap.prototype._createMap = function (mapOptions) {
if (this.map) this.destroy();
// get center and zoom from mapdata, or use defaults
this._mapCenter = _.get(this._geoJson, 'properties.center') || defaultMapCenter;
this._mapZoom = _.get(this._geoJson, 'properties.zoom') || defaultMapZoom;
// add map tiles layer, using the mapTiles object settings
if (this._attr.wms && this._attr.wms.enabled) {
this._tileLayer = L.tileLayer.wms(this._attr.wms.url, this._attr.wms.options);

View file

@ -98,11 +98,17 @@ export default function TileMapFactory(Private) {
* @param selection {Object} d3 selection
*/
TileMap.prototype._appendMap = function (selection) {
let container = $(selection).addClass('tilemap');
const container = $(selection).addClass('tilemap');
const uiStateParams = this.handler.vis ? {
mapCenter: this.handler.vis.uiState.get('mapCenter'),
mapZoom: this.handler.vis.uiState.get('mapZoom')
} : {};
let map = new TileMapMap(container, this._chartData, {
// center: this._attr.mapCenter,
// zoom: this._attr.mapZoom,
const params = _.assign({}, _.get(this._chartData, 'geoAgg.vis.params'), uiStateParams);
const map = new TileMapMap(container, this._chartData, {
center: params.mapCenter,
zoom: params.mapZoom,
events: this.events,
markerType: this._attr.mapType,
tooltipFormatter: this.tooltipFormatter,

View file

@ -29,8 +29,7 @@ uiModules
},
template: visualizeTemplate,
link: function ($scope, $el, attr) {
let chart; // set in "vis" watcher
let minVisChartHeight = 180;
const minVisChartHeight = 180;
if (_.isUndefined($scope.showSpyPanel)) {
$scope.showSpyPanel = true;

View file

@ -1,4 +1,4 @@
import { defaultsDeep } from 'lodash';
import { defaultsDeep, partial } from 'lodash';
import defaultsProvider from './defaults';
export default function setupSettings(kbnServer, server, config) {
@ -23,12 +23,19 @@ export default function setupSettings(kbnServer, server, config) {
return Promise.resolve(defaultsProvider());
}
function userSettingsNotFound(kibanaVersion) {
const message = 'Could not find user-provided settings for this version of Kibana (' + kibanaVersion + ')';
server.plugins.kibana.status.red(message);
return {};
}
function getUserProvided() {
const { client } = server.plugins.elasticsearch;
const clientSettings = getClientSettings(config);
return client
.get({ ...clientSettings })
.then(res => res._source)
.catch(partial(userSettingsNotFound, clientSettings.id))
.then(user => hydrateUserSettings(user));
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{".kibana":{"mappings":{"config":{"properties":{"buildNum":{"type":"keyword"}}},"index-pattern":{"properties":{"fields":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"timeFieldName":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"title":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}},"search":{"properties":{"columns":{"type":"text"},"description":{"type":"text"},"hits":{"type":"integer"},"kibanaSavedObjectMeta":{"properties":{"searchSourceJSON":{"type":"text"}}},"sort":{"type":"text"},"title":{"type":"text"},"version":{"type":"integer"}}},"visualization":{"properties":{"description":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"kibanaSavedObjectMeta":{"properties":{"searchSourceJSON":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}},"title":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"uiStateJSON":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"version":{"type":"integer"},"visState":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}},"server":{"properties":{"uuid":{"type":"keyword"}}},"dashboard":{"properties":{"description":{"type":"text"},"hits":{"type":"integer"},"kibanaSavedObjectMeta":{"properties":{"searchSourceJSON":{"type":"text"}}},"optionsJSON":{"type":"text"},"panelsJSON":{"type":"text"},"timeFrom":{"type":"text"},"timeRestore":{"type":"boolean"},"timeTo":{"type":"text"},"title":{"type":"text"},"uiStateJSON":{"type":"text"},"version":{"type":"integer"}}}}}}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{".kibana":{"mappings":{"index-pattern":{"properties":{"fields":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"timeFieldName":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"title":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}}}}}

View file

@ -2,8 +2,6 @@ import {
bdd,
scenarioManager,
common,
// settingsPage,
// headerPage,
consolePage
} from '../../../support';

View file

@ -8,10 +8,6 @@ import { bdd, remote, scenarioManager, defaultTimeout } from '../../../support';
return remote.setWindowSize(1200,800);
});
bdd.after(function unloadMakelogs() {
return scenarioManager.unload('logstashFunctional');
});
require('./_console');
});
}());

View file

@ -0,0 +1,139 @@
import {
bdd,
common,
dashboardPage,
headerPage,
scenarioManager,
esClient,
elasticDump
} from '../../../support';
(function () {
var expect = require('expect.js');
(function () {
bdd.describe('dashboard tab', function describeIndexTests() {
bdd.before(function () {
common.debug('Starting dashboard before method');
var logstash = scenarioManager.loadIfEmpty('logstashFunctional');
return esClient.delete('.kibana')
.then(function () {
return common.try(function () {
return esClient.updateConfigDoc({'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*'});
});
})
// and load a set of makelogs data
.then(function loadkibanaVisualizations() {
common.debug('load kibana index with visualizations');
return elasticDump.elasticLoad('dashboard','.kibana');
})
.then(function () {
common.debug('navigateToApp dashboard');
return common.navigateToApp('dashboard');
})
// wait for the logstash data load to finish if it hasn't already
.then(function () {
return logstash;
})
.catch(common.handleError(this));
});
bdd.describe('add visualizations to dashboard', function dashboardTest() {
var visualizations = ['Visualization漢字 AreaChart',
'Visualization☺漢字 DataTable',
'Visualization漢字 LineChart',
'Visualization PieChart',
'Visualization TileMap',
'Visualization☺ VerticalBarChart',
'Visualization MetricChart'
];
bdd.it('should be able to add visualizations to dashboard', function addVisualizations() {
function addVisualizations(arr) {
return arr.reduce(function (promise, vizName) {
return promise
.then(function () {
return dashboardPage.addVisualization(vizName);
});
}, Promise.resolve());
}
return addVisualizations(visualizations)
.then(function () {
common.debug('done adding visualizations');
});
});
bdd.it('set the timepicker time to that which contains our test data', function setTimepicker() {
var fromTime = '2015-09-19 06:31:44.000';
var toTime = '2015-09-23 18:31:44.000';
var testSubName = 'Dashboard Test 1';
// .then(function () {
common.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
return headerPage.setAbsoluteRange(fromTime, toTime)
.then(function sleep() {
return common.sleep(4000);
})
.then(function takeScreenshot() {
common.debug('Take screenshot');
common.saveScreenshot('./screenshot-' + testSubName + '.png');
})
.catch(common.handleError(this));
});
bdd.it('should save and load dashboard', function saveAndLoadDashboard() {
var testSubName = 'Dashboard Test 1';
// TODO: save time on the dashboard and test it
return dashboardPage.saveDashboard(testSubName)
// click New Dashboard just to clear the one we just created
.then(function () {
return dashboardPage.clickNewDashboard();
})
.then(function () {
return dashboardPage.loadSavedDashboard(testSubName);
})
.catch(common.handleError(this));
});
bdd.it('should have all the expected visualizations', function checkVisualizations() {
return common.tryForTime(10000, function () {
return dashboardPage.getPanelTitles()
.then(function (panelTitles) {
common.log('visualization titles = ' + panelTitles);
expect(panelTitles).to.eql(visualizations);
});
})
.catch(common.handleError(this));
});
bdd.it('should have all the expected initial sizes', function checkVisualizationSizes() {
var visObjects = [ { dataCol: '1', dataRow: '1', dataSizeX: '3', dataSizeY: '2', title: 'Visualization漢字 AreaChart' },
{ dataCol: '4', dataRow: '1', dataSizeX: '3', dataSizeY: '2', title: 'Visualization☺漢字 DataTable' },
{ dataCol: '7', dataRow: '1', dataSizeX: '3', dataSizeY: '2', title: 'Visualization漢字 LineChart' },
{ dataCol: '10', dataRow: '1', dataSizeX: '3', dataSizeY: '2', title: 'Visualization PieChart' },
{ dataCol: '1', dataRow: '3', dataSizeX: '3', dataSizeY: '2', title: 'Visualization TileMap' },
{ dataCol: '4', dataRow: '3', dataSizeX: '3', dataSizeY: '2', title: 'Visualization☺ VerticalBarChart' },
{ dataCol: '7', dataRow: '3', dataSizeX: '3', dataSizeY: '2', title: 'Visualization MetricChart' }
];
return common.tryForTime(10000, function () {
return dashboardPage.getPanelData()
.then(function (panelTitles) {
common.log('visualization titles = ' + panelTitles);
expect(panelTitles).to.eql(visObjects);
});
})
.catch(common.handleError(this));
});
});
});
}());
}());

View file

@ -0,0 +1,13 @@
import { bdd, remote, scenarioManager, defaultTimeout } from '../../../support';
(function () {
bdd.describe('dashboard app', function () {
this.timeout = defaultTimeout;
bdd.before(function () {
return remote.setWindowSize(1200,800);
});
require('./_dashboard');
});
}());

View file

@ -152,7 +152,7 @@ import {
];
return discoverPage.setChartInterval(chartInterval)
.then(function () {
return common.sleep(8000);
return common.sleep(4000);
})
.then(function () {
return verifyChartData(expectedBarChartData);
@ -167,7 +167,7 @@ import {
];
return discoverPage.setChartInterval(chartInterval)
.then(function () {
return common.sleep(8000);
return common.sleep(4000);
})
.then(function () {
return verifyChartData(expectedBarChartData);
@ -188,6 +188,26 @@ import {
.catch(common.handleError(this));
});
bdd.it('browser back button should show previous interval Daily', function () {
var expectedChartInterval = 'Daily';
var expectedBarChartData = [
'133.196', '129.192', '129.724'
];
return this.remote.goBack()
.then(function () {
return common.try(function tryingForTime() {
return discoverPage.getChartInterval()
.then(function (actualInterval) {
expect(actualInterval).to.be(expectedChartInterval);
});
});
})
.then(function () {
return verifyChartData(expectedBarChartData);
})
.catch(common.handleError(this));
});
bdd.it('should show correct data for chart interval Monthly', function () {
var chartInterval = 'Monthly';
var expectedBarChartData = [ '122.535'];
@ -241,6 +261,12 @@ import {
.catch(common.handleError(this));
});
bdd.it('should not show "no results"', () => {
return discoverPage.hasNoResults().then(visible => {
expect(visible).to.be(false);
})
.catch(common.handleError(this));
});
function verifyChartData(expectedBarChartData) {
return common.try(function tryingForTime() {
@ -270,6 +296,69 @@ import {
}
});
bdd.describe('query #2, which has an empty time range', function () {
var fromTime = '1999-06-11 09:22:11.000';
var toTime = '1999-06-12 11:21:04.000';
bdd.before(() => {
common.debug('setAbsoluteRangeForAnotherQuery');
return headerPage
.setAbsoluteRange(fromTime, toTime)
.catch(common.handleError(this));
});
bdd.it('should show "no results"', () => {
return discoverPage.hasNoResults().then(visible => {
expect(visible).to.be(true);
})
.catch(common.handleError(this));
});
bdd.it('should suggest a new time range is picked', () => {
return discoverPage.hasNoResultsTimepicker().then(visible => {
expect(visible).to.be(true);
})
.catch(common.handleError(this));
});
bdd.it('should open and close the time picker', () => {
let i = 0;
return closeTimepicker() // close
.then(() => isTimepickerOpen(false)
.then(el => el.click()) // open
.then(() => isTimepickerOpen(true))
.then(el => el.click()) // close
.then(() => isTimepickerOpen(false))
.catch(common.handleError(this))
);
function closeTimepicker() {
return headerPage.isTimepickerOpen().then(shown => {
if (!shown) {
return;
}
return discoverPage
.getNoResultsTimepicker()
.click(); // close
});
}
function isTimepickerOpen(expected) {
return headerPage.isTimepickerOpen().then(shown => {
common.debug(`expect (#${++i}) timepicker to be ${peek(expected)} (is ${peek(shown)}).`);
expect(shown).to.be(expected);
return discoverPage.getNoResultsTimepicker();
function peek(state) {
return state ? 'open' : 'closed';
}
});
}
});
});
});
}());
}());

View file

@ -16,29 +16,8 @@ import {
var fromTime = '2015-09-19 06:31:44.000';
var toTime = '2015-09-23 18:31:44.000';
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickAreaChart');
return visualizePage.clickAreaChart();

View file

@ -11,23 +11,11 @@ import {
(function () {
bdd.describe('visualize app', function describeIndexTests() {
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
.catch(common.handleError(this));
});
bdd.before(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
});
bdd.describe('chart types', function indexPatternCreation() {

View file

@ -16,29 +16,8 @@ import {
var toTime = '2015-09-23 18:31:44.000';
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickDataTable');
return visualizePage.clickDataTable();

View file

@ -16,29 +16,8 @@ import {
var fromTime = '2015-09-19 06:31:44.000';
var toTime = '2015-09-23 18:31:44.000';
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickLineChart');
return visualizePage.clickLineChart();

View file

@ -21,29 +21,8 @@ import {
common.debug('Start of test' + testSubName + 'Visualization');
var vizName1 = 'Visualization ' + testSubName;
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickMetric');
return visualizePage.clickMetric();

View file

@ -16,29 +16,8 @@ import {
var fromTime = '2015-09-19 06:31:44.000';
var toTime = '2015-09-23 18:31:44.000';
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickPieChart');
return visualizePage.clickPieChart();

View file

@ -17,29 +17,8 @@ import {
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickTileMap');
return visualizePage.clickTileMap();

View file

@ -16,29 +16,8 @@ import {
var toTime = '2015-09-23 18:31:44.000';
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
common.debug('navigateTo');
return settingsPage.navigateTo().then(settingsPage.clickExistingIndicesAddDataLink);
})
.then(function () {
common.debug('createIndexPattern');
return settingsPage.createIndexPattern();
})
.then(function () {
return settingsPage.clickAdvancedTab();
})
.then(function GetAdvancedSetting() {
common.debug('check for required UTC timezone');
return settingsPage.getAdvancedSettings('dateFormat:tz');
})
.then(function (advancedSetting) {
expect(advancedSetting).to.be('UTC');
})
.then(function () {
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize');
})
common.debug('navigateToApp visualize');
return common.navigateToApp('visualize')
.then(function () {
common.debug('clickVerticalBarChart');
return visualizePage.clickVerticalBarChart();

View file

@ -1,4 +1,12 @@
import { bdd, remote, common, defaultTimeout, scenarioManager } from '../../../support';
import {
bdd,
remote,
common,
defaultTimeout,
scenarioManager,
esClient,
elasticDump
} from '../../../support';
(function () {
bdd.describe('visualize app', function () {
@ -7,14 +15,24 @@ import { bdd, remote, common, defaultTimeout, scenarioManager } from '../../../s
bdd.before(function () {
var self = this;
remote.setWindowSize(1200,800);
// load a set of makelogs data
common.debug('loadIfEmpty logstashFunctional ' + self.timeout);
return scenarioManager.loadIfEmpty('logstashFunctional');
});
bdd.after(function unloadMakelogs() {
return scenarioManager.unload('logstashFunctional');
common.debug('Starting visualize before method');
var logstash = scenarioManager.loadIfEmpty('logstashFunctional');
return esClient.delete('.kibana')
.then(function () {
return common.try(function () {
return esClient.updateConfigDoc({'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*'});
});
})
.then(function loadkibanaIndexPattern() {
common.debug('load kibana index with default index pattern');
return elasticDump.elasticLoad('visualize','.kibana');
})
// wait for the logstash data load to finish if it hasn't already
.then(function () {
return logstash;
})
.catch(common.handleError(this));
});
require('./_chart_types');

View file

@ -24,7 +24,8 @@ define(function (require) {
'intern/dojo/node!./status_page',
'intern/dojo/node!./apps/settings',
'intern/dojo/node!./apps/visualize',
'intern/dojo/node!./apps/console'
'intern/dojo/node!./apps/console',
'intern/dojo/node!./apps/dashboard'
], function () {});
});
});

View file

@ -16,7 +16,7 @@ import { bdd, common } from '../../support';
.findByCssSelector('.plugin_status_breakdown')
.getVisibleText()
.then(function (text) {
expect(text.indexOf('plugin:kibana Ready')).to.be.above(-1);
expect(text.indexOf('kibana 1.0.0 Ready')).to.be.above(-1);
});
})
.catch(common.handleError(self));

View file

@ -0,0 +1,109 @@
import { common, config} from './';
export default (function () {
var util = require('util');
var path = require('path');
var url = require('url');
var resolve = require('path').resolve;
var Elasticdump = require('elasticdump').elasticdump;
function ElasticDump() {
}
ElasticDump.prototype = {
/*
** This function is basically copied from
** https://github.com/taskrabbit/elasticsearch-dump/blob/master/bin/elasticdump
** and allows calling elasticdump for importing or exporting data from Elasticsearch
*/
elasticdumpModule: function elasticdumpModule(myinput, myoutput, index, mytype) {
var options = {
limit: 100,
offset: 0,
debug: false,
type: mytype,
delete: false,
all: false,
maxSockets: null,
input: myinput,
'input-index': null,
output: myoutput,
'output-index': index,
inputTransport: null,
outputTransport: null,
searchBody: null,
sourceOnly: false,
jsonLines: false,
format: '',
'ignore-errors': false,
scrollTime: '10m',
timeout: null,
skip: null,
toLog: null,
};
var dumper = new Elasticdump(options.input, options.output, options);
dumper.on('log', function (message) { common.debug(message); });
dumper.on('error', function (error) { common.debug('error', 'Error Emitted => ' + (error.message || JSON.stringify(error))); });
var promise = new Promise(function (resolve, reject) {
dumper.dump(function (error, totalWrites) {
if (error) {
common.debug('THERE WAS AN ERROR :-(');
reject(Error(error));
} else {
resolve ('elasticdumpModule success');
}
});
});
return promise;
},
/*
** Dumps data from Elasticsearch into json files.
** Takes a simple filename as input like 'dashboard' (for dashboard tests).
** Appends ''.mapping.json' and '.data.json' for the actual filenames.
** Writes files to the Kibana root dir.
** Fails if the files already exist, so consider appending a timestamp to filename.
*/
elasticDump: function elasticDump(index, file) {
var self = this;
common.debug('Dumping mapping from ' + url.format(config.servers.elasticsearch) + '/' + index
+ ' to (' + file + '.mapping.json)');
return this.elasticdumpModule(url.format(config.servers.elasticsearch),
file + '.mapping.json', index, 'mapping')
.then(function () {
common.debug('Dumping data from ' + url.format(config.servers.elasticsearch) + '/' + index
+ ' to (' + file + '.data.json)');
return self.elasticdumpModule(url.format(config.servers.elasticsearch),
file + '.data.json', index, 'data');
});
},
/*
** Loads data from json files into Elasticsearch.
** Takes a simple filename as input like 'dashboard' (for dashboard tests).
** Appends ''.mapping.json' and '.data.json' for the actual filenames.
** Path /test/fixtures/dump_data is hard-coded
*/
elasticLoad: function elasticLoad(file, index) {
// TODO: should we have a flag to delete the index first?
// or use scenarioManager.unload(index) ? <<- currently this
var self = this;
common.debug('Loading mapping (test/fixtures/dump_data/' + file + '.mapping.json) into '
+ url.format(config.servers.elasticsearch) + '/' + index);
return this.elasticdumpModule('test/fixtures/dump_data/' + file + '.mapping.json',
url.format(config.servers.elasticsearch), index, 'mapping')
.then(function () {
common.debug('Loading data (test/fixtures/dump_data/' + file + '.data.json) into '
+ url.format(config.servers.elasticsearch) + '/' + index);
return self.elasticdumpModule('test/fixtures/dump_data/' + file + '.data.json',
url.format(config.servers.elasticsearch), index, 'data');
});
},
};
return ElasticDump;
}());

96
test/support/es_client.js Normal file
View file

@ -0,0 +1,96 @@
import { common, remote} from './';
export default (function () {
var elasticsearch = require('elasticsearch');
var Promise = require('bluebird');
function EsClient(server) {
this.remote = remote;
if (!server) throw new Error('No server defined');
// NOTE: some large sets of test data can take several minutes to load
this.client = new elasticsearch.Client({
host: server,
requestTimeout: 300000,
defer: function () {
return Promise.defer();
}
});
}
EsClient.prototype = {
constructor: EsClient,
/**
* Delete an index
* @param {string} index
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
delete: function (index) {
return this.client.indices.delete({
index: index
})
.catch(function (reason) {
// if the index never existed yet, or was already deleted it's OK
if (reason.message.indexOf('index_not_found_exception') < 0) {
common.debug('reason.message: ' + reason.message);
throw reason;
}
});
},
/**
* Add fields to the config doc (like setting timezone and defaultIndex)
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
updateConfigDoc: function (docMap) {
// first we need to get the config doc's id so we can use it in our _update call
var self = this;
var configId;
var docMapString = JSON.stringify(docMap);
return this.client.search({
index: '.kibana',
type: 'config'
})
.then(function (response) {
if (response.errors) {
throw new Error(
'get config failed\n' +
response.items
.map(i => i[Object.keys(i)[0]].error)
.filter(Boolean)
.map(err => ' ' + JSON.stringify(err))
.join('\n')
);
} else {
configId = response.hits.hits[0]._id;
common.debug('config._id =' + configId);
}
})
// now that we have the id, we can update
// return scenarioManager.updateConfigDoc({'dateFormat:tz':'UTC', 'defaultIndex':'logstash-*'});
.then(function (response) {
common.debug('updating config with ' + docMapString);
return self.client.update({
index: '.kibana',
type: 'config',
id: configId,
body: {
'doc':
docMap
}
});
})
.catch(function (err) {
throw err;
});
}
};
return EsClient;
}());

View file

@ -1,10 +1,13 @@
import url from 'url';
import EsClient from './es_client';
import ElasticDump from './elastic_dump';
import ScenarioManager from '../fixtures/scenario_manager';
import Common from './pages/common';
import DiscoverPage from './pages/discover_page';
import SettingsPage from './pages/settings_page';
import HeaderPage from './pages/header_page';
import VisualizePage from './pages/visualize_page';
import DashboardPage from './pages/dashboard_page';
import ShieldPage from './pages/shield_page';
import ConsolePage from './pages/console_page';
@ -17,6 +20,7 @@ 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));
exports.esClient = new EsClient(url.format(exports.config.servers.elasticsearch));
defineDelayedExport('remote', (suite) => suite.remote);
defineDelayedExport('common', () => new Common());
@ -24,8 +28,10 @@ defineDelayedExport('discoverPage', () => new DiscoverPage());
defineDelayedExport('headerPage', () => new HeaderPage());
defineDelayedExport('settingsPage', () => new SettingsPage());
defineDelayedExport('visualizePage', () => new VisualizePage());
defineDelayedExport('dashboardPage', () => new DashboardPage());
defineDelayedExport('shieldPage', () => new ShieldPage());
defineDelayedExport('consolePage', () => new ConsolePage());
defineDelayedExport('elasticDump', () => new ElasticDump());
// creates an export for values that aren't actually avaialable until
// until tests start to run. These getters will throw errors if the export

View file

@ -1,4 +1,4 @@
import { common, config, defaultTryTimeout, defaultFindTimeout, remote, shieldPage } from '../';
import { config, defaultTryTimeout, defaultFindTimeout, remote, shieldPage } from '../';
export default (function () {
var Promise = require('bluebird');
@ -280,6 +280,7 @@ export default (function () {
.setFindTimeout(defaultFindTimeout)
.findDisplayedByCssSelector(testSubjSelector(selector));
}
};
return Common;

View file

@ -0,0 +1,200 @@
import { remote, common, defaultFindTimeout } from '../';
export default (function () {
var thisTime;
function DashboardPage() {
this.remote = remote;
thisTime = this.remote.setFindTimeout(defaultFindTimeout);
}
DashboardPage.prototype = {
constructor: DashboardPage,
clickNewDashboard: function clickNewDashboard() {
return thisTime
.findByCssSelector('button.ng-scope[aria-label="New Dashboard"]')
.click();
},
clickAddVisualization: function clickAddVisualization() {
return thisTime
.findByCssSelector('button.ng-scope[aria-label="Add a panel to the dashboard"]')
.click();
},
filterVizNames: function filterVizNames(vizName) {
return thisTime
.findByCssSelector('input[placeholder="Visualizations Filter..."]')
.click()
.pressKeys(vizName);
},
clickVizNameLink: function clickVizNameLink(vizName) {
return thisTime
.findByLinkText(vizName)
.click();
},
closeAddVizualizationPanel: function closeAddVizualizationPanel() {
common.debug('-------------close panel');
return thisTime
.findByCssSelector('i.fa fa-chevron-up')
.click();
},
addVisualization: function addVisualization(vizName) {
var self = this;
return this.clickAddVisualization()
.then(function () {
common.debug('filter visualization (' + vizName + ')');
return self.filterVizNames(vizName);
})
// this second wait is usually enough to avoid the
// 'stale element reference: element is not attached to the page document'
// on the next step
.then(function () {
return common.sleep(1000);
})
.then(function () {
// but wrap in a try loop since it can still happen
return common.try(function () {
common.debug('click visualization (' + vizName + ')');
return self.clickVizNameLink(vizName);
});
})
// this second click of 'Add' collapses the Add Visualization pane
.then(function () {
return self.clickAddVisualization();
});
},
saveDashboard: function saveDashboard(dashName) {
var self = this;
return thisTime
.findByCssSelector('button.ng-scope[aria-label="Save Dashboard"]')
.click()
.then(function () {
return common.sleep(1000);
})
.then(function () {
common.debug('saveButton button clicked');
return thisTime
.findById('dashboardTitle')
.type(dashName);
})
// click save button
.then(function () {
return thisTime
.findByCssSelector('.btn-primary')
.click();
})
// verify that green message at the top of the page.
// it's only there for about 5 seconds
.then(function () {
return thisTime
.findByCssSelector('kbn-truncated.toast-message.ng-isolate-scope')
.getVisibleText();
});
},
clickDashboardByLinkText: function clickDashboardByLinkText(dashName) {
return thisTime
.findByLinkText(dashName)
.click();
},
// use the search filter box to narrow the results down to a single
// entry, or at least to a single page of results
loadSavedDashboard: function loadSavedDashboard(dashName) {
var self = this;
return thisTime
.findByCssSelector('button.ng-scope[aria-label="Load Saved Dashboard"]')
.click()
.then(function filterDashboard() {
common.debug('Load Saved Dashboard button clicked');
return self.remote
.findByCssSelector('input[name="filter"]')
.click()
.type(dashName.replace('-',' '));
})
.then(function () {
return common.sleep(1000);
})
.then(function clickDashboardByLinkedText() {
return self
.clickDashboardByLinkText(dashName);
});
},
getPanelTitles: function getPanelTitles() {
common.debug('in getPanelTitles');
return thisTime
.findAllByCssSelector('span.panel-title')
.then(function (titleObjects) {
function getTitles(chart) {
return chart.getAttribute('title');
}
var getTitlePromises = titleObjects.map(getTitles);
return Promise.all(getTitlePromises);
});
},
getPanelData: function getPanelData() {
common.debug('in getPanelData');
return thisTime
.findAllByCssSelector('li.gs-w')
.then(function (titleObjects) {
function getTitles(chart) {
var obj = {};
return chart.getAttribute('data-col')
.then(function (theData) {
obj = {dataCol:theData};
return chart;
})
.then(function (chart) {
return chart.getAttribute('data-row')
.then(function (theData) {
obj.dataRow = theData;
return chart;
});
})
.then(function (chart) {
return chart.getAttribute('data-sizex')
.then(function (theData) {
obj.dataSizeX = theData;
return chart;
});
})
.then(function (chart) {
return chart.getAttribute('data-sizey')
.then(function (theData) {
obj.dataSizeY = theData;
return chart;
});
})
.then(function (chart) {
return chart.findByCssSelector('span.panel-title')
.then(function (titleElement) {
return titleElement.getAttribute('title');
})
.then(function (theData) {
obj.title = theData;
return obj;
});
});
}
var getTitlePromises = titleObjects.map(getTitles);
return Promise.all(getTitlePromises);
});
}
};
return DashboardPage;
}());

View file

@ -100,7 +100,21 @@ export default (function () {
getChartInterval: function getChartInterval() {
return thisTime
.findByCssSelector('a[ng-click="toggleInterval()"]')
.getVisibleText();
.getVisibleText()
.then(function (intervalText) {
if (intervalText.length > 0) {
return intervalText;
} else {
return thisTime
.findByCssSelector('select[ng-model="state.interval"]')
.getProperty('value') // this gets 'string:d' for Daily
.then(function (selectedValue) {
return thisTime
.findByCssSelector('option[value="' + selectedValue + '"]')
.getVisibleText();
});
}
});
},
setChartInterval: function setChartInterval(interval) {
@ -211,6 +225,24 @@ export default (function () {
return thisTime
.findByClassName('sidebar-list')
.getProperty('clientWidth');
},
hasNoResults: function hasNoResults() {
return common
.findTestSubject('discoverNoResults')
.then(() => true)
.catch(() => false);
},
getNoResultsTimepicker: function getNoResultsTimepicker() {
return common.findTestSubject('discoverNoResultsTimefilter');
},
hasNoResultsTimepicker: function hasNoResultsTimepicker() {
return this
.getNoResultsTimepicker()
.then(() => true)
.catch(() => false);
}
};

View file

@ -48,6 +48,13 @@ export default (function () {
.findDisplayedByClassName('navbar-timepicker-time-desc').click();
},
isTimepickerOpen: function isTimepickerOpen() {
return this.remote.setFindTimeout(defaultFindTimeout)
.findDisplayedByCssSelector('.kbn-timepicker')
.then(() => true)
.catch(() => false);
},
clickAbsoluteButton: function clickAbsoluteButton() {
return this.remote.setFindTimeout(defaultFindTimeout)
.findByLinkText('Absolute').click();