Merge branch 'fix/failTestOnBadBulk' into upgrade/elasticsearch/master

This commit is contained in:
spalger 2016-02-08 14:05:52 -07:00
commit 10e97a4d75
31 changed files with 690 additions and 328 deletions

View file

@ -147,6 +147,7 @@ Distributable packages can be found in `target/` after the build completes.
Packages are built using fpm, pleaserun, dpkg, and rpm. fpm and pleaserun can be installed using gem. Package building has only been tested on Linux and is not supported on any other platform.
```sh
gem install pleaserun
apt-get install ruby-dev
gem install fpm
npm run build:ospackages
```

View file

@ -0,0 +1,31 @@
import expect from 'expect.js';
import fileType, { ZIP, TAR } from '../file_type';
describe('kibana cli', function () {
describe('file_type', function () {
it('returns ZIP for .zip filename', function () {
const type = fileType('wat.zip');
expect(type).to.equal(ZIP);
});
it('returns TAR for .tar.gz filename', function () {
const type = fileType('wat.tar.gz');
expect(type).to.equal(TAR);
});
it('returns TAR for .tgz filename', function () {
const type = fileType('wat.tgz');
expect(type).to.equal(TAR);
});
it('returns undefined for unknown file type', function () {
const type = fileType('wat.unknown');
expect(type).to.equal(undefined);
});
it('accepts paths', function () {
const type = fileType('/some/path/to/wat.zip');
expect(type).to.equal(ZIP);
});
it('accepts urls', function () {
const type = fileType('http://example.com/wat.zip');
expect(type).to.equal(ZIP);
});
});
});

View file

@ -124,6 +124,25 @@ describe('kibana cli', function () {
});
});
it('should consider .tgz files as archive type .tar.gz', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.tar.gz');
const couchdb = nock('http://www.files.com')
.defaultReplyHeaders({
'content-length': '10'
})
.get('/plugin.tgz')
.replyWithFile(200, filePath);
const sourceUrl = 'http://www.files.com/plugin.tgz';
return downloader._downloadSingle(sourceUrl)
.then(function (data) {
expect(data.archiveType).to.be('.tar.gz');
expectWorkingPathNotEmpty();
});
});
it('should download a zip from a valid http url', function () {
const filePath = join(__dirname, 'replies/test_plugin_master.zip');

View file

@ -1,5 +1,6 @@
import getProgressReporter from '../progress_reporter';
import { createWriteStream, createReadStream, unlinkSync, statSync } from 'fs';
import fileType from '../file_type';
function openSourceFile({ sourcePath }) {
try {
@ -36,15 +37,6 @@ async function copyFile({ readStream, writeStream, progressReporter }) {
});
}
function getArchiveTypeFromFilename(path) {
if (/\.zip$/i.test(path)) {
return '.zip';
}
if (/\.tar\.gz$/i.test(path)) {
return '.tar.gz';
}
}
/*
// Responsible for managing local file transfers
*/
@ -67,7 +59,7 @@ export default async function copyLocalFile(logger, sourcePath, targetPath) {
}
// all is well, return our archive type
const archiveType = getArchiveTypeFromFilename(sourcePath);
const archiveType = fileType(sourcePath);
return { archiveType };
} catch (err) {
logger.error(err);

View file

@ -2,6 +2,7 @@ import Wreck from 'wreck';
import getProgressReporter from '../progress_reporter';
import { fromNode as fn } from 'bluebird';
import { createWriteStream, unlinkSync } from 'fs';
import fileType, { ZIP, TAR } from '../file_type';
function sendRequest({ sourceUrl, timeout }) {
const maxRedirects = 11; //Because this one goes to 11.
@ -49,18 +50,12 @@ function getArchiveTypeFromResponse(resp, sourceUrl) {
const contentType = (resp.headers['content-type'] || '');
switch (contentType.toLowerCase()) {
case 'application/zip': return '.zip';
case 'application/x-gzip': return '.tar.gz';
case 'application/zip': return ZIP;
case 'application/x-gzip': return TAR;
default:
//If we can't infer the archive type from the content-type header,
//fall back to checking the extension in the url
if (/\.zip$/i.test(sourceUrl)) {
return '.zip';
}
if (/\.tar\.gz$/i.test(sourceUrl)) {
return '.tar.gz';
}
break;
return fileType(sourceUrl);
}
}

View file

@ -0,0 +1,14 @@
export const TAR = '.tar.gz';
export const ZIP = '.zip';
export default function fileType(filename) {
if (/\.zip$/i.test(filename)) {
return ZIP;
}
if (/\.tar\.gz$/i.test(filename)) {
return TAR;
}
if (/\.tgz$/i.test(filename)) {
return TAR;
}
}

View file

@ -1,12 +1,13 @@
import zipExtract from './extractors/zip';
import tarGzExtract from './extractors/tar_gz';
import { ZIP, TAR } from './file_type';
export default function extractArchive(settings, logger, archiveType) {
switch (archiveType) {
case '.zip':
case ZIP:
return zipExtract(settings, logger);
break;
case '.tar.gz':
case TAR:
return tarGzExtract(settings, logger);
break;
default:

View file

@ -0,0 +1,13 @@
module.exports = function (kibana) {
return new kibana.Plugin({
uiExports: {
docViews: [
'plugins/kbn_doc_views/kbn_doc_views'
]
}
});
};

View file

@ -0,0 +1,4 @@
{
"name": "kbn_doc_views",
"version": "1.0.0"
}

View file

@ -0,0 +1,167 @@
import angular from 'angular';
import _ from 'lodash';
import sinon from 'auto-release-sinon';
import expect from 'expect.js';
import ngMock from 'ngMock';
import $ from 'jquery';
import 'ui/render_directive';
import 'plugins/kbn_doc_views/views/table';
import docViewsRegistry from 'ui/registry/doc_views';
const hit = {
'_index': 'logstash-2014.09.09',
'_type': 'apache',
'_id': '61',
'_score': 1,
'_source': {
'extension': 'html',
'bytes': 100,
'area': [{lat: 7, lon: 7}],
'noMapping': 'hasNoMapping',
'objectArray': [{foo: true}, {bar: false}],
'_underscore': 1
}
};
// Load the kibana app dependencies.
let $parentScope;
let $scope;
let indexPattern;
let flattened;
let docViews;
const init = function ($elem, props) {
ngMock.inject(function ($rootScope, $compile) {
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
$elem.scope().$digest();
$scope = $elem.isolateScope();
});
};
const destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
};
describe('docViews', function () {
let $elem;
let initView;
beforeEach(ngMock.module('kibana'));
beforeEach(function () {
const aggs = 'index-pattern="indexPattern" hit="hit" filter="filter"';
$elem = angular.element(`<render-directive ${aggs} definition="view.directive"></render-directive>`);
ngMock.inject(function (Private) {
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
flattened = indexPattern.flattenHit(hit);
docViews = Private(docViewsRegistry);
});
initView = function initView(view) {
$elem.append(view.directive.template);
init($elem, {
indexPattern: indexPattern,
hit: hit,
view: view,
filter: sinon.spy()
});
};
});
afterEach(function () {
destroy();
});
describe('Table', function () {
beforeEach(function () {
initView(docViews.byName.Table);
});
it('should have a row for each field', function () {
const rows = $elem.find('tr');
expect($elem.find('tr').length).to.be(_.keys(flattened).length);
});
it('should have the field name in the first column', function () {
_.each(_.keys(flattened), function (field) {
expect($elem.find('td[title="' + field + '"]').length).to.be(1);
});
});
it('should have the a value for each field', function () {
_.each(_.keys(flattened), function (field) {
const cellValue = $elem.find('td[title="' + field + '"]').siblings().find('.doc-viewer-value').text();
// This sucks, but testing the filter chain is too hairy ATM
expect(cellValue.length).to.be.greaterThan(0);
});
});
describe('filtering', function () {
it('should apply a filter when clicking filterable fields', function () {
const cell = $elem.find('td[title="bytes"]').next();
cell.find('.fa-search-plus').first().click();
expect($scope.filter.calledOnce).to.be(true);
cell.find('.fa-search-minus').first().click();
expect($scope.filter.calledTwice).to.be(true);
});
it('should NOT apply a filter when clicking non-filterable fields', function () {
const cell = $elem.find('td[title="area"]').next();
cell.find('.fa-search-plus').first().click();
expect($scope.filter.calledOnce).to.be(false);
cell.find('.fa-search-minus').first().click();
expect($scope.filter.calledTwice).to.be(false);
});
});
describe('warnings', function () {
it('displays a warning about field name starting with underscore', function () {
const cells = $elem.find('td[title="_underscore"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(1);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about missing mappings', function () {
const cells = $elem.find('td[title="noMapping"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(1);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about objects in arrays', function () {
const cells = $elem.find('td[title="objectArray"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(1);
});
});
});
describe('JSON', function () {
beforeEach(function () {
initView(docViews.byName.JSON);
});
it('has pretty JSON', function () {
expect($scope.hitJson).to.equal(angular.toJson(hit, true));
});
it('should have a global ACE object', function () {
expect(window.ace).to.be.a(Object);
});
it('should have one ACE div', function () {
expect($elem.find('div[id="json-ace"]').length).to.be(1);
});
it('should contain the same code as hitJson', function () {
const editor = window.ace.edit($elem.find('div[id="json-ace"]')[0]);
const code = editor.getSession().getValue();
expect(code).to.equal($scope.hitJson);
});
});
});

View file

@ -0,0 +1,2 @@
import 'plugins/kbn_doc_views/views/table';
import 'plugins/kbn_doc_views/views/json';

View file

@ -0,0 +1,16 @@
<div
id="json-ace"
ng-model="hitJson"
readonly
ui-ace="{
useWrapMode: true,
onLoad: aceLoaded,
advanced: {
highlightActiveLine: false
},
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}"></div>

View file

@ -0,0 +1,26 @@
import _ from 'lodash';
import angular from 'angular';
import 'ace';
import docViewsRegistry from 'ui/registry/doc_views';
import jsonHtml from './json.html';
docViewsRegistry.register(function () {
return {
title: 'JSON',
order: 20,
directive: {
template: jsonHtml,
scope: {
hit: '='
},
controller: function ($scope) {
$scope.hitJson = angular.toJson($scope.hit, true);
$scope.aceLoaded = (editor) => {
editor.$blockScrolling = Infinity;
};
}
}
};
});

View file

@ -0,0 +1,49 @@
<table class="table table-condensed">
<tbody>
<tr ng-repeat="field in fields">
<td field-name="field"
field-type="mapping[field].type"
width="1%"
class="doc-viewer-field">
</td>
<td width="1%" class="doc-viewer-buttons" ng-if="filter">
<span ng-if="mapping[field].filterable">
<i ng-click="filter(mapping[field], flattened[field], '+')"
tooltip="Filter for value"
tooltip-append-to-body="1"
class="fa fa-search-plus"></i>
<i ng-click="filter(mapping[field], flattened[field],'-')"
tooltip="Filter out value"
tooltip-append-to-body="1"
class="fa fa-search-minus"></i>
</span>
<span ng-if="!mapping[field].filterable" tooltip="Unindexed fields can not be searched">
<i class="fa fa-search-plus text-muted"></i>
<i class="fa fa-search-minus text-muted"></i>
</span>
<span ng-if="columns">
<i ng-click="toggleColumn(field)"
tooltip="Toggle column in table"
tooltip-append-to-body="1"
class="fa fa-columns"></i>
</span>
</td>
<td>
<i ng-if="!mapping[field] && field[0] === '_'"
tooltip-placement="top"
tooltip="Field names beginning with _ are not supported"
class="fa fa-warning text-color-warning ng-scope doc-viewer-underscore"></i>
<i ng-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="No cached mapping for this field. Refresh field list from the Settings > Indices page"
class="fa fa-warning text-color-warning ng-scope doc-viewer-no-mapping"></i>
<i ng-if="showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="Objects in arrays are not well supported."
class="fa fa-warning text-color-warning ng-scope doc-viewer-object-array"></i>
<div class="doc-viewer-value" ng-bind-html="typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,35 @@
import _ from 'lodash';
import docViewsRegistry from 'ui/registry/doc_views';
import tableHtml from './table.html';
docViewsRegistry.register(function () {
return {
title: 'Table',
order: 10,
directive: {
template: tableHtml,
scope: {
hit: '=',
indexPattern: '=',
filter: '=',
columns: '='
},
controller: function ($scope) {
$scope.mapping = $scope.indexPattern.fields.byName;
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
$scope.formatted = $scope.indexPattern.formatHit($scope.hit);
$scope.fields = _.keys($scope.flattened).sort();
$scope.toggleColumn = function (fieldName) {
_.toggleInOut($scope.columns, fieldName);
};
$scope.showArrayInObjectsWarning = function (row, field) {
var value = $scope.flattened[field];
return _.isArray(value) && typeof value[0] === 'object';
};
}
}
};
});

View file

@ -22,7 +22,8 @@ module.exports = function (kibana) {
'spyModes',
'fieldFormats',
'navbarExtensions',
'settingsSections'
'settingsSections',
'docViews'
],
injectVars: function (server, options) {

View file

@ -29,7 +29,7 @@
</label>
<a ng-disabled="selectedItems.length == 0"
confirm-click="bulkDelete()"
confirmation="Are you sure want to delete the selected {{currentTab.title}}? This action is irreversible!"
confirmation="Are you sure you want to delete the selected {{currentTab.title}}? This action is irreversible!"
class="btn btn-xs btn-danger" aria-label="Delete"><i aria-hidden="true" class="fa fa-trash"></i> Delete</a>
<a ng-disabled="selectedItems.length == 0"
ng-click="bulkExport()"

View file

@ -11,17 +11,17 @@ define(function (require) {
},
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html" target="_blank">Options</a> for the lucene query string parser',
type: 'json'
},
'sort:options': {
value: '{ "unmapped_type": "boolean" }',
description: 'Options the Elasticsearch sort parameter',
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html" target="_blank">Options</a> for the Elasticsearch sort parameter',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
description: 'When displaying a pretty formatted date, use this <a href="http://momentjs.com/docs/#/displaying/format/" target="_blank">format</a>',
},
'dateFormat:tz': {
value: 'Browser',
@ -104,7 +104,7 @@ define(function (require) {
}
}, null, ' '),
type: 'json',
description: 'Default properties for the WMS map server support in the tile map'
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',
@ -159,22 +159,27 @@ define(function (require) {
'format:number:defaultPattern': {
type: 'string',
value: '0,0.[000]',
description: 'Default numeral format for the "number" format'
description: 'Default <a href="http://numeraljs.com/" target="_blank">numeral format</a> for the "number" format'
},
'format:bytes:defaultPattern': {
type: 'string',
value: '0,0.[000]b',
description: 'Default numeral format for the "bytes" format'
description: 'Default <a href="http://numeraljs.com/" target="_blank">numeral format</a> for the "bytes" format'
},
'format:percent:defaultPattern': {
type: 'string',
value: '0,0.[000]%',
description: 'Default numeral format for the "percent" format'
description: 'Default <a href="http://numeraljs.com/" target="_blank">numeral format</a> for the "percent" format'
},
'format:currency:defaultPattern': {
type: 'string',
value: '($0,0.[00])',
description: 'Default numeral format for the "currency" format'
description: 'Default <a href="http://numeraljs.com/" target="_blank">numeral format</a> for the "currency" format'
},
'savedObjects:perPage': {
type: 'number',
value: 5,
description: 'Number of objects to show per page in the load dialog'
},
'timepicker:timeDefaults': {
type: 'json',

View file

@ -4,7 +4,7 @@ import keymap from 'ui/utils/key_map';
define(function (require) {
var module = require('ui/modules').get('kibana');
module.directive('savedObjectFinder', function ($location, $injector, kbnUrl, Private) {
module.directive('savedObjectFinder', function ($location, $injector, kbnUrl, Private, config) {
var services = Private(require('ui/saved_objects/saved_object_registry')).byLoaderPropertiesName;
@ -23,6 +23,9 @@ define(function (require) {
controller: function ($scope, $element, $timeout) {
var self = this;
// The number of items to show in the list
$scope.perPage = config.get('savedObjects:perPage');
// the text input element
var $input = $element.find('input[ng-model=filter]');

View file

@ -1,160 +1,80 @@
import angular from 'angular';
import _ from 'lodash';
import sinon from 'auto-release-sinon';
import expect from 'expect.js';
import ngMock from 'ngMock';
import $ from 'jquery';
import 'ui/private';
import docViewsRegistry from 'ui/registry/doc_views';
import Registry from 'ui/registry/_registry';
import 'ui/doc_viewer';
var hit = {
'_index': 'logstash-2014.09.09',
'_type': 'apache',
'_id': '61',
'_score': 1,
'_source': {
'extension': 'html',
'bytes': 100,
'area': [{lat: 7, lon: 7}],
'noMapping': 'hasNoMapping',
'objectArray': [{foo: true}, {bar: false}],
'_underscore': 1
}
};
// Load the kibana app dependencies.
var $parentScope;
var $scope;
var indexPattern;
var flattened;
var init = function ($elem, props) {
ngMock.inject(function ($rootScope, $compile) {
$parentScope = $rootScope;
_.assign($parentScope, props);
$compile($elem)($parentScope);
$elem.scope().$digest();
$scope = $elem.isolateScope();
});
};
var destroy = function () {
$scope.$destroy();
$parentScope.$destroy();
};
describe('docViewer', function () {
var $elem;
let $rootScope;
let $compile;
let stubRegistry;
let $elem;
let init;
beforeEach(ngMock.module('kibana'));
beforeEach(function () {
$elem = angular.element('<doc-viewer index-pattern="indexPattern" hit="hit" filter="filter"></doc-viewer>');
ngMock.inject(function (Private) {
indexPattern = Private(require('fixtures/stubbed_logstash_index_pattern'));
flattened = indexPattern.flattenHit(hit);
ngMock.module('kibana', function (PrivateProvider) {
stubRegistry = new Registry({
index: ['name'],
order: ['order'],
constructor() {
this.forEach(docView => {
docView.shouldShow = docView.shouldShow || _.constant(true);
docView.name = docView.name || docView.title;
});
}
});
PrivateProvider.swap(docViewsRegistry, stubRegistry);
});
init($elem, {
indexPattern: indexPattern,
hit: hit,
filter: sinon.spy()
// Create the scope
ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
});
});
afterEach(function () {
destroy();
});
describe('Table mode', function () {
it('should have a row for each field', function () {
var rows = $elem.find('tr');
expect($elem.find('tr').length).to.be(_.keys(flattened).length);
});
it('should have the field name in the first column', function () {
_.each(_.keys(flattened), function (field) {
expect($elem.find('td[title="' + field + '"]').length).to.be(1);
beforeEach(function () {
$elem = angular.element('<doc-viewer></doc-viewer>');
init = function init() {
ngMock.inject(function ($rootScope, $compile) {
$compile($elem)($rootScope);
$elem.scope().$digest();
return $elem;
});
});
it('should have the a value for each field', function () {
_.each(_.keys(flattened), function (field) {
var cellValue = $elem.find('td[title="' + field + '"]').siblings().find('.doc-viewer-value').text();
// This sucks, but testing the filter chain is too hairy ATM
expect(cellValue.length).to.be.greaterThan(0);
});
});
describe('filtering', function () {
it('should apply a filter when clicking filterable fields', function () {
var cell = $elem.find('td[title="bytes"]').next();
cell.find('.fa-search-plus').first().click();
expect($scope.filter.calledOnce).to.be(true);
cell.find('.fa-search-minus').first().click();
expect($scope.filter.calledTwice).to.be(true);
});
it('should NOT apply a filter when clicking non-filterable fields', function () {
var cell = $elem.find('td[title="area"]').next();
cell.find('.fa-search-plus').first().click();
expect($scope.filter.calledOnce).to.be(false);
cell.find('.fa-search-minus').first().click();
expect($scope.filter.calledTwice).to.be(false);
});
});
describe('warnings', function () {
it('displays a warning about field name starting with underscore', function () {
var cells = $elem.find('td[title="_underscore"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(1);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about missing mappings', function () {
var cells = $elem.find('td[title="noMapping"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(1);
expect(cells.find('.doc-viewer-object-array').length).to.be(0);
});
it('displays a warning about objects in arrays', function () {
var cells = $elem.find('td[title="objectArray"]').siblings();
expect(cells.find('.doc-viewer-underscore').length).to.be(0);
expect(cells.find('.doc-viewer-no-mapping').length).to.be(0);
expect(cells.find('.doc-viewer-object-array').length).to.be(1);
});
});
};
});
describe('JSON mode', function () {
it('has pretty JSON', function () {
expect($scope.hitJson).to.equal(angular.toJson(hit, true));
describe('injecting views', function () {
function registerExtension(def = {}) {
stubRegistry.register(function () {
return _.defaults(def, {
title: 'exampleView',
order: 0,
directive: {
template: `Example`
}
});
});
}
it('should have a tab for the view', function () {
registerExtension();
registerExtension({title: 'exampleView2'});
init();
expect($elem.find('.nav-tabs li').length).to.be(2);
});
it('should have a global ACE object', function () {
expect(window.ace).to.be.a(Object);
});
it('should have one ACE div', function () {
expect($elem.find('div[id="json-ace"]').length).to.be(1);
});
it('should contain the same code as hitJson', function () {
var editor = window.ace.edit($elem.find('div[id="json-ace"]')[0]);
var code = editor.getSession().getValue();
expect(code).to.equal($scope.hitJson);
it('should activate the first view in order', function () {
registerExtension({order: 2});
registerExtension({title: 'exampleView2'});
init();
expect($elem.find('.nav-tabs .active').text().trim()).to.be('exampleView2');
});
});
});

View file

@ -1,76 +0,0 @@
<div class="doc-viewer">
<ul class="nav nav-tabs">
<li ng-class="{active: mode == 'table'}"><a ng-click="mode='table'">Table</a></li>
<li ng-class="{active: mode == 'json'}"><a ng-click="mode='json'">JSON</a></li>
</ul>
<div class="doc-viewer-content">
<table class="table table-condensed" ng-show="mode == 'table'">
<tbody>
<tr ng-repeat="field in fields">
<td field-name="field"
field-type="mapping[field].type"
width="1%"
class="doc-viewer-field">
</td>
<td width="1%" class="doc-viewer-buttons" ng-if="filter">
<span ng-if="mapping[field].filterable">
<i ng-click="filter(mapping[field], flattened[field], '+')"
tooltip="Filter for value"
tooltip-append-to-body="1"
class="fa fa-search-plus"></i>
<i ng-click="filter(mapping[field], flattened[field],'-')"
tooltip="Filter out value"
tooltip-append-to-body="1"
class="fa fa-search-minus"></i>
</span>
<span ng-if="!mapping[field].filterable" tooltip="Unindexed fields can not be searched">
<i class="fa fa-search-plus text-muted"></i>
<i class="fa fa-search-minus text-muted"></i>
</span>
<span ng-if="columns">
<i ng-click="toggleColumn(field)"
tooltip="Toggle column in table"
tooltip-append-to-body="1"
class="fa fa-columns"></i>
</span>
</td>
<td>
<i ng-if="!mapping[field] && field[0] === '_'"
tooltip-placement="top"
tooltip="Field names beginning with _ are not supported"
class="fa fa-warning text-color-warning ng-scope doc-viewer-underscore"></i>
<i ng-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="No cached mapping for this field. Refresh field list from the Settings > Indices page"
class="fa fa-warning text-color-warning ng-scope doc-viewer-no-mapping"></i>
<i ng-if="showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="Objects in arrays are not well supported."
class="fa fa-warning text-color-warning ng-scope doc-viewer-object-array"></i>
<div class="doc-viewer-value" ng-bind-html="typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>
</table>
<div
id="json-ace"
ng-show="mode == 'json'"
ng-model="hitJson"
readonly
ui-ace="{
useWrapMode: true,
onLoad: aceLoaded,
advanced: {
highlightActiveLine: false
},
rendererOptions: {
showPrintMargin: false,
maxLines: 4294967296
},
mode: 'json'
}"></div>
</div>
</div>

View file

@ -1,48 +1,45 @@
import _ from 'lodash';
import angular from 'angular';
import 'ace';
import html from 'ui/doc_viewer/doc_viewer.html';
import $ from 'jquery';
import uiModules from 'ui/modules';
import 'ui/doc_viewer/doc_viewer.less';
define(function (require) {
import DocViews from 'ui/registry/doc_views';
require('ui/modules').get('kibana')
.directive('docViewer', function (config, Private) {
return {
restrict: 'E',
template: html,
scope: {
hit: '=',
indexPattern: '=',
filter: '=?',
columns: '=?'
},
link: {
pre($scope) {
$scope.aceLoaded = (editor) => {
editor.$blockScrolling = Infinity;
};
},
post($scope, $el, attr) {
// If a field isn't in the mapping, use this
$scope.mode = 'table';
$scope.mapping = $scope.indexPattern.fields.byName;
$scope.flattened = $scope.indexPattern.flattenHit($scope.hit);
$scope.hitJson = angular.toJson($scope.hit, true);
$scope.formatted = $scope.indexPattern.formatHit($scope.hit);
$scope.fields = _.keys($scope.flattened).sort();
$scope.toggleColumn = function (fieldName) {
_.toggleInOut($scope.columns, fieldName);
};
$scope.showArrayInObjectsWarning = function (row, field) {
var value = $scope.flattened[field];
return _.isArray(value) && typeof value[0] === 'object';
};
}
}
};
});
uiModules.get('kibana')
.directive('docViewer', function (config, Private) {
const docViews = Private(DocViews);
return {
restrict: 'E',
scope: {
hit: '=',
indexPattern: '=',
filter: '=?',
columns: '=?'
},
template: function ($el, $attr) {
const $viewer = $('<div class="doc-viewer">');
$el.append($viewer);
const $tabs = $('<ul class="nav nav-tabs">');
const $content = $('<div class="doc-viewer-content">');
$viewer.append($tabs);
$viewer.append($content);
docViews.inOrder.forEach(view => {
const $tab = $(`<li ng-show="docViews['${view.name}'].shouldShow(hit)" ng-class="{active: mode == '${view.name}'}">
<a ng-click="mode='${view.name}'">${view.title}</a>
</li>`);
$tabs.append($tab);
const $viewAttrs = 'hit="hit" index-pattern="indexPattern" filter="filter" columns="columns"';
const $ext = $(`<render-directive ${$viewAttrs} ng-show="mode == '${view.name}'" definition="docViews['${view.name}'].directive">
</render-directive>`);
$ext.html(view.directive.template);
$content.append($ext);
});
return $el.html();
},
controller: function ($scope) {
$scope.mode = docViews.inOrder[0].name;
$scope.docViews = docViews.byName;
}
};
});

View file

@ -17,7 +17,7 @@
<span class="finder-hit-count"><strong>{{finder.hitCount}}</strong> {{finder.hitCountNoun()}}</span>
</div>
</form>
<paginate list="finder.hits" per-page="5">
<paginate list="finder.hits" per-page="{{perPage}}">
<ul
class="list-group list-group-menu"
ng-class="{'select-mode': finder.selector.enabled}">

View file

@ -0,0 +1,14 @@
import _ from 'lodash';
define(function (require) {
return require('ui/registry/_registry')({
name: 'docViews',
index: ['name'],
order: ['order'],
constructor() {
this.forEach(docView => {
docView.shouldShow = docView.shouldShow || _.constant(true);
docView.name = docView.name || docView.title;
});
}
});
});

View file

@ -4,35 +4,37 @@ import expect from 'expect.js';
import ngMock from 'ngMock';
import 'ui/render_directive';
let $parentScope;
let $elem;
let $directiveScope;
function init(markup = '', definition = {}) {
ngMock.module('kibana/render_directive');
// Create the scope
ngMock.inject(function ($rootScope, $compile) {
$parentScope = $rootScope;
// create the markup
$elem = angular.element('<render-directive>');
$elem.html(markup);
if (definition !== null) {
$parentScope.definition = definition;
$elem.attr('definition', 'definition');
}
// compile the directive
$compile($elem)($parentScope);
$parentScope.$apply();
$directiveScope = $elem.isolateScope();
});
}
let init;
let $rootScope;
let $compile;
describe('render_directive', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
init = function init(markup = '', definition = {}) {
const $parentScope = $rootScope;
// create the markup
const $elem = angular.element('<render-directive>');
$elem.html(markup);
if (definition !== null) {
$parentScope.definition = definition;
$elem.attr('definition', 'definition');
}
// compile the directive
$compile($elem)($parentScope);
$parentScope.$apply();
const $directiveScope = $elem.isolateScope();
return { $parentScope, $directiveScope, $elem };
};
}));
describe('directive requirements', function () {
it('should throw if not given a definition', function () {
expect(() => init('', null)).to.throwException(/must have a definition/);
@ -62,4 +64,40 @@ describe('render_directive', function () {
sinon.assert.callCount(definition.controller, 1);
});
});
describe('definition scope binding', function () {
it('should accept two-way, attribute, and expression binding directives', function () {
const $el = angular.element(`
<render-directive
definition="definition"
two-way-prop="parentTwoWay"
attr="Simple Attribute"
expr="parentExpression()"
>
{{two}},{{attr}},{{expr()}}
</render-directive>
`);
const $parentScope = $rootScope.$new();
$parentScope.definition = {
scope: {
two: '=twoWayProp',
attr: '@',
expr: '&expr'
}
};
$parentScope.parentTwoWay = true;
$parentScope.parentExpression = function () {
return !$parentScope.parentTwoWay;
};
$compile($el)($parentScope);
$parentScope.$apply();
expect($el.text().trim()).to.eql('true,Simple Attribute,false');
$parentScope.parentTwoWay = false;
$parentScope.$apply();
expect($el.text().trim()).to.eql('false,Simple Attribute,true');
});
});
});

View file

@ -0,0 +1,40 @@
import { forOwn, noop } from 'lodash';
import 'ui/bind';
const bindingRE = /^(=|=\?|&|@)([a-zA-Z0-9_$]+)?$/;
export default function ($parse) {
return function (bindings, $scope, $attrs) {
forOwn(bindings, (binding, local) => {
if (!bindingRE.test(binding)) {
throw new Error(`Invalid scope binding "${binding}". Expected it to match ${bindingRE}`);
}
const [, type, attribute = local] = binding.match(bindingRE);
const attr = $attrs[attribute];
switch (type) {
case '=':
$scope.$bind(local, attr);
break;
case '=?':
throw new Error('<render-directive> does not currently support optional two-way bindings.');
break;
case '&':
if (attr) {
const getter = $parse(attr);
$scope[local] = function () {
return getter($scope.$parent);
};
} else {
$scope[local] = noop;
}
break;
case '@':
$scope[local] = attr;
$attrs.$observe(attribute, v => $scope[local] = v);
break;
}
});
};
}

View file

@ -1,8 +1,36 @@
import _ from 'lodash';
import { isPlainObject } from 'lodash';
import $ from 'jquery';
const module = require('ui/modules').get('kibana/render_directive');
module.directive('renderDirective', function () {
import uiModules from 'ui/modules';
import applyScopeBindingsProvider from './apply_scope_bindings';
/**
* The <render-directive> directive is useful for programaticaly modifying or
* extending a view. It allows defining the majority of the directives behavior
* using a "definition" object, which the implementer can obtain from plugins (for instance).
*
* The definition object supports the parts of a directive definition that are
* easy enough to implement without having to hack angular, and does it's best to
* make sure standard directive life-cycle timing is respected.
*
* @param [Object] definition - the external configuration for this directive to assume
* @param [Function] definition.controller - a constructor used to create the controller for this directive
* @param [String] definition.controllerAs - a name where the controller should be stored on scope
* @param [Object] definition.scope - an object defining the binding properties for values read from
* attributes and bound to $scope. The keys of this object are the
* local names of $scope properties, and the values are a combination
* of the binding style (=, @, or &) and the external attribute name.
* See [the Angular docs]
* (https://code.angularjs.org/1.4.9/docs/api/ng/service/$compile#-scope-)
* for more info
* @param [Object|Function] definition.link - either a post link function or an object with pre and/or
* post link functions.
*/
uiModules
.get('kibana')
.directive('renderDirective', function (Private, $parse) {
const applyScopeBindings = Private(applyScopeBindingsProvider);
return {
restrict: 'E',
scope: {
@ -14,17 +42,26 @@ module.directive('renderDirective', function () {
controller: function ($scope, $element, $attrs, $transclude) {
if (!$scope.definition) throw new Error('render-directive must have a definition attribute');
const { controller, controllerAs } = $scope.definition;
const { controller, controllerAs, scope } = $scope.definition;
applyScopeBindings(scope, $scope, $attrs);
if (controller) {
if (controllerAs) $scope[controllerAs] = this;
$scope.$eval(controller, { $scope, $element, $attrs, $transclude });
}
},
link: function ($scope, $el, $attrs) {
const { link } = $scope.definition;
if (link) {
link($scope, $el, $attrs);
}
link: {
pre($scope, $el, $attrs, controller) {
const { link } = $scope.definition;
const preLink = isPlainObject(link) ? link.pre : null;
if (preLink) preLink($scope, $el, $attrs, controller);
},
post($scope, $el, $attrs, controller) {
const { link } = $scope.definition;
const postLink = isPlainObject(link) ? link.post : link;
if (postLink) postLink($scope, $el, $attrs, controller);
},
}
};
});
});

View file

@ -36,7 +36,7 @@ class UiApp {
getModules() {
return _.chain([
this.uiExports.find(_.get(this, 'spec.uses', [])),
this.uiExports.find(['chromeNavControls']),
this.uiExports.find(['chromeNavControls', 'sledgehammers']),
])
.flatten()
.uniq()

View file

@ -63,6 +63,8 @@ class UiExports {
case 'chromeNavControls':
case 'navbarExtensions':
case 'settingsSections':
case 'docViews':
case 'sledgehammers':
return (plugin, spec) => {
this.aliases[type] = _.union(this.aliases[type] || [], spec);
};

View file

@ -24,6 +24,10 @@ module.exports = function (grunt) {
'--name', 'kibana',
'--description', 'Explore\ and\ visualize\ your\ Elasticsearch\ data.',
'--version', version,
'--url', 'https://www.elastic.co',
'--vendor', 'Elasticsearch,\ Inc.',
'--maintainer', 'Kibana Team\ \<info@elastic.co\>',
'--license', 'Apache\ 2.0',
'--after-install', resolve(userScriptsDir, 'installer.sh'),
'--after-remove', resolve(userScriptsDir, 'remover.sh'),
'--config-files', '/opt/kibana/config/kibana.yml'
@ -42,7 +46,7 @@ module.exports = function (grunt) {
grunt.file.mkdir(targetDir);
if (buildDeb || noneSpecified) {
fpm(args.concat('-t', 'deb', '-a', arch, files, sysv, systemd));
fpm(args.concat('-t', 'deb', '--deb-priority', 'optional', '-a', arch, files, sysv, systemd));
}
if (buildRpm || noneSpecified) {
fpm(args.concat('-t', 'rpm', '-a', arch, '--rpm-os', 'linux', files, sysv, systemd));

View file

@ -45,6 +45,18 @@ ScenarioManager.prototype.load = function (id) {
body: body
});
})
.then(function (response) {
if (response.errors) {
throw new Error(
'bulk failed\n' +
response.items
.map(i => i[Object.keys(i)[0]].error)
.filter(Boolean)
.map(err => ' ' + JSON.stringify(err))
.join('\n')
);
}
})
.catch(function (err) {
if (bulk.haltOnFailure === false) return;
throw err;