mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Merge branch 'fix/failTestOnBadBulk' into upgrade/elasticsearch/master
This commit is contained in:
commit
10e97a4d75
31 changed files with 690 additions and 328 deletions
|
@ -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
|
||||
```
|
||||
|
|
31
src/cli/plugin/__tests__/file_type.js
Normal file
31
src/cli/plugin/__tests__/file_type.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
src/cli/plugin/file_type.js
Normal file
14
src/cli/plugin/file_type.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
13
src/plugins/kbn_doc_views/index.js
Normal file
13
src/plugins/kbn_doc_views/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
module.exports = function (kibana) {
|
||||
|
||||
return new kibana.Plugin({
|
||||
|
||||
uiExports: {
|
||||
docViews: [
|
||||
'plugins/kbn_doc_views/kbn_doc_views'
|
||||
]
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
4
src/plugins/kbn_doc_views/package.json
Normal file
4
src/plugins/kbn_doc_views/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "kbn_doc_views",
|
||||
"version": "1.0.0"
|
||||
}
|
167
src/plugins/kbn_doc_views/public/__tests__/doc_views.js
Normal file
167
src/plugins/kbn_doc_views/public/__tests__/doc_views.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
2
src/plugins/kbn_doc_views/public/kbn_doc_views.js
Normal file
2
src/plugins/kbn_doc_views/public/kbn_doc_views.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import 'plugins/kbn_doc_views/views/table';
|
||||
import 'plugins/kbn_doc_views/views/json';
|
16
src/plugins/kbn_doc_views/public/views/json.html
Normal file
16
src/plugins/kbn_doc_views/public/views/json.html
Normal 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>
|
26
src/plugins/kbn_doc_views/public/views/json.js
Normal file
26
src/plugins/kbn_doc_views/public/views/json.js
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
49
src/plugins/kbn_doc_views/public/views/table.html
Normal file
49
src/plugins/kbn_doc_views/public/views/table.html
Normal 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>
|
35
src/plugins/kbn_doc_views/public/views/table.js
Normal file
35
src/plugins/kbn_doc_views/public/views/table.js
Normal 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';
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -22,7 +22,8 @@ module.exports = function (kibana) {
|
|||
'spyModes',
|
||||
'fieldFormats',
|
||||
'navbarExtensions',
|
||||
'settingsSections'
|
||||
'settingsSections',
|
||||
'docViews'
|
||||
],
|
||||
|
||||
injectVars: function (server, options) {
|
||||
|
|
|
@ -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()"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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]');
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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}">
|
||||
|
|
14
src/ui/public/registry/doc_views.js
Normal file
14
src/ui/public/registry/doc_views.js
Normal 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;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
40
src/ui/public/render_directive/apply_scope_bindings.js
Normal file
40
src/ui/public/render_directive/apply_scope_bindings.js
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
12
test/fixtures/scenario_manager.js
vendored
12
test/fixtures/scenario_manager.js
vendored
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue