Merge branch 'master' of github.com:elastic/kibana into ignorePrivateFields

This commit is contained in:
Spencer Alger 2015-05-01 10:46:23 -07:00
commit c4408feb8a
108 changed files with 1977 additions and 828 deletions

View file

@ -22,6 +22,13 @@ Please make sure you have signed the [Contributor License Agreement](http://www.
```sh
npm install -g grunt-cli bower
```
- Clone the kibana repo and move into it
```sh
git clone https://github.com/elastic/kibana.git kibana
cd kibana
```
- Install node and bower dependencies

View file

@ -9,7 +9,7 @@
</span>
<div class="hintbox" ng-show="showAnalyzedFieldWarning">
<p>
<strong>Careful!</strong> The field selected contains analyzed strings. Values such as <i>foo-bar</i> will be broken into <i>foo</i> and <i>bar</i>. See <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-core-types.html" target="_blank">Mapping Core Types</a> for more information on setting this field as <i>not_analyzed</i>
<strong>Careful!</strong> The field selected contains analyzed strings. Analyzed strings are highly unique and can use a lot of memory to visualize. Values such as <i>foo-bar</i> will be broken into <i>foo</i> and <i>bar</i>. See <a href="http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-core-types.html" target="_blank">Mapping Core Types</a> for more information on setting this field as <i>not_analyzed</i>
</p>
<p ng-show="indexedFields.byName[agg.params.field.name + '.raw'].analyzed == false">
@ -22,9 +22,17 @@
name="field"
required
ng-model="agg.params.field"
ng-if="indexedFields.length"
auto-select-if-only-one="indexedFields"
ng-options="field as field.displayName group by field.type for field in indexedFields"
ng-change="aggParam.onChange(agg)">
</select>
<div class="hintbox" ng-if="!indexedFields.length">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>No Compatible Fields:</strong> The "{{ vis.indexPattern.id }}" index pattern does not any of the following field types: {{ agg.type.params.byName.field.filterFieldTypes | commaList:false }}
</p>
</div>
</div>

View file

@ -17,6 +17,15 @@
</div>
</div>
</div>
<input ng-model="agg.params.filters.length" name="filterLength" required min="1" type="number" class="ng-hide">
<div class="hintbox" ng-show="aggForm.filterLength.$invalid">
<p>
<i class="fa fa-danger text-danger"></i>
<strong>Required:</strong> You must specify at least one filter
</p>
</div>
<div
click-focus="'filter'+(agg.params.filters.length-1)"
ng-click="agg.params.filters.push({input:{}})"

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Values</label>
<kbn-number-list
ng-model="agg.params.values"
unit-name="value"
range="[-Infinity,Infinity]"
>
</kbn-number-list>
</div>

View file

@ -0,0 +1,9 @@
<div class="form-group">
<label>Percents</label>
<kbn-number-list
ng-model="agg.params.percents"
unit-name="percent"
range="[0,100]"
>
</kbn-number-list>
</div>

View file

@ -1,31 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.percents.controller">
<label>Percentiles</label>
<div
ng-repeat="value in agg.params.percents track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.percents[$index]"
values-list="agg.params.percents"
values-list-min="0"
values-list-max="100"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<p ng-show="aggForm.validLength.$invalid" class="text-danger text-center">
You mush specify at least one percentile
</p>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add Percent
</button>
</div>

View file

@ -3,7 +3,11 @@
<label>JSON Input</label>
<i class="fa fa-info-circle"></i>
</span>
<div class="hintbox" ng-show="showJsonHint">Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example <i>shard_size</i> on a <i>terms</i> aggregation</div>
<div class="hintbox" ng-show="showJsonHint">
<p>
Any JSON formatted properties you add here will be merged with the elasticsearch aggregation definition for this section. For example <i>shard_size</i> on a <i>terms</i> aggregation
</p>
</div>
<p>
<textarea
type="text"
@ -12,4 +16,4 @@
validate-json
></textarea>
</p>
</div>
</div>

View file

@ -1,30 +0,0 @@
<div class="form-group" ng-controller="agg.type.params.byName.values.controller">
<label>Values</label>
<div
ng-repeat="value in agg.params.values track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="agg.params.values[$index]"
values-list="agg.params.values"
values-list-min="0"
input-focus
class="form-control">
<button type="button" ng-click="remove($index, 1)" class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
<input ng-model="validLength" name="validLength" required type="hidden">
<p ng-show="aggForm.validLength.$invalid" class="text-danger text-center">
You must specify at least one value
</p>
<button
ng-click="add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add value
</button>
</div>

View file

@ -5,8 +5,9 @@ define(function (require) {
var MetricAggType = Private(require('components/agg_types/metrics/_metric_agg_type'));
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
require('components/agg_types/controls/_values_list');
var valuesEditor = require('text!components/agg_types/controls/values.html');
var valuesEditor = require('text!components/agg_types/controls/percentile_ranks.html');
// required by the values editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,20 +29,7 @@ define(function (require) {
{
name: 'values',
editor: valuesEditor,
default: [],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.values.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.values.push(_.last($scope.agg.params.values) + 1);
};
$scope.$watchCollection('agg.params.values', function (values) {
$scope.validLength = _.size(values) || null;
});
}
default: []
}
],
getResponseAggs: function (agg) {
@ -60,4 +48,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -6,8 +6,9 @@ define(function (require) {
var getResponseAggConfig = Private(require('components/agg_types/metrics/_get_response_agg_config'));
var ordinalSuffix = require('utils/ordinal_suffix');
require('components/agg_types/controls/_values_list');
var percentEditor = require('text!components/agg_types/controls/percents.html');
var percentsEditor = require('text!components/agg_types/controls/percentiles.html');
// required by the percentiles editor
require('components/number_list/number_list');
var valueProps = {
makeLabel: function () {
@ -28,21 +29,8 @@ define(function (require) {
},
{
name: 'percents',
editor: percentEditor,
default: [1, 5, 25, 50, 75, 95, 99],
controller: function ($scope) {
$scope.remove = function (index) {
$scope.agg.params.percents.splice(index, 1);
};
$scope.add = function () {
$scope.agg.params.percents.push(_.last($scope.agg.params.percents) + 1);
};
$scope.$watchCollection('agg.params.percents', function (percents) {
$scope.validLength = _.size(percents) || null;
});
}
editor: percentsEditor,
default: [1, 5, 25, 50, 75, 95, 99]
}
],
getResponseAggs: function (agg) {
@ -61,4 +49,4 @@ define(function (require) {
}
});
};
});
});

View file

@ -0,0 +1,26 @@
define(function (require) {
var _ = require('lodash');
require('modules')
.get('kibana')
.filter('commaList', function () {
/**
* Angular filter that accepts either an array or a comma-seperated string
* and outputs either an array, or a comma-seperated string for presentation.
*
* @param {String|Array} input - The comma-seperated list or array
* @param {Boolean} inclusive - Should the list be joined with an "and"?
* @return {String}
*/
return function (input, inclusive) {
var list = _.commaSeperatedList(input);
if (list.length < 2) {
return list.join('');
}
var conj = inclusive ? ' and ' : ' or ';
return list.slice(0, -1).join(', ') + conj + _.last(list);
};
});
});

View file

@ -45,7 +45,7 @@ define(function (require) {
queue.forEach(function (q) { q.reject(err); });
})
.finally(function () {
$rootScope.$emit('change:config', updated.concat(deleted));
$rootScope.$broadcast('change:config', updated.concat(deleted));
});
};
@ -70,7 +70,7 @@ define(function (require) {
var defer = Promise.defer();
queue.push(defer);
notify.log('config change: ' + key + ': ' + oldVal + ' -> ' + newVal);
$rootScope.$emit('change:config.' + key, newVal, oldVal);
$rootScope.$broadcast('change:config.' + key, newVal, oldVal);
// reset the fire timer
clearTimeout(timer);
@ -80,4 +80,4 @@ define(function (require) {
};
};
});
});

View file

@ -19,7 +19,7 @@ define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var defaults = require('components/config/defaults');
var defaults = Private(require('components/config/defaults'));
var DelayedUpdater = Private(require('components/config/_delayed_updater'));
var vals = Private(require('components/config/_vals'));
@ -123,6 +123,29 @@ define(function (require) {
if (updater) updater.fire();
};
/**
* A little helper for binding config variables to $scopes
*
* @param {Scope} $scope - an angular $scope object
* @param {string} key - the config key to bind to
* @param {string} [property] - optional property name where the value should
* be stored. Defaults to the config key
* @return {function} - an unbind function
*/
config.$bind = function ($scope, key, property) {
if (!property) property = key;
var update = function () {
$scope[property] = config.get(key);
};
update();
return _.partial(_.invoke, [
$scope.$on('change:config.' + key, update),
$scope.$on('init:config', update)
], 'call');
};
/*****
* PRIVATE API
*****/

View file

@ -1,86 +1,93 @@
define(function (require) {
var _ = require('lodash');
return function () {
var _ = require('lodash');
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 7,
description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' +
'12 is the max. Explanation of cell dimensions: http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
}
return {
'query:queryString:options': {
value: '{ "analyze_wildcard": true }',
description: 'Options for the lucene query string parser',
type: 'json'
},
'dateFormat': {
value: 'MMMM Do YYYY, HH:mm:ss.SSS',
description: 'When displaying a pretty formatted date, use this format',
},
'dateFormat:scaled': {
type: 'json',
value:
'[\n' +
' ["", "hh:mm:ss.SSS"],\n' +
' ["PT1S", "HH:mm:ss"],\n' +
' ["PT1M", "HH:mm"],\n' +
' ["PT1H",\n' +
' "YYYY-MM-DD HH:mm"],\n' +
' ["P1DT", "YYYY-MM-DD"],\n' +
' ["P1YT", "YYYY"]\n' +
']',
description: 'Values that define the format used in situations where timebased' +
' data is rendered in order, and formatted timestamps should adapt to the' +
' interval between measurements. Keys are ISO 8601 intervals:' +
' http://en.wikipedia.org/wiki/ISO_8601#Time_intervals'
},
'defaultIndex': {
value: null,
description: 'The index to access if no index is set',
},
'metaFields': {
value: ['_source', '_id', '_type', '_index'],
description: 'Fields that exist outside of _source to merge into our document when displaying it',
},
'discover:sampleSize': {
value: 500,
description: 'The number of rows to show in the table',
},
'fields:popularLimit': {
value: 10,
description: 'The top N most popular fields to show',
},
'format:numberPrecision': {
value: 3,
description: 'Round numbers to this many decimal places',
},
'histogram:barTarget': {
value: 50,
description: 'Attempt to generate around this many bar when using "auto" interval in date histograms',
},
'histogram:maxBars': {
value: 100,
description: 'Never show more than this many bar in date histograms, scale values if needed',
},
'visualization:tileMap:maxPrecision': {
value: 7,
description: 'The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, ' +
'12 is the max. Explanation of cell dimensions: http://www.elastic.co/guide/en/elasticsearch/reference/current/' +
'search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator',
},
'csv:separator': {
value: ',',
description: 'Separate exported values with this string',
},
'csv:quoteValues': {
value: true,
description: 'Should values be quoted in csv exports?',
},
'history:limit': {
value: 10,
description: 'In fields that have history (e.g. query inputs), show this many recent values',
},
'shortDots:enable': {
value: false,
description: 'Shorten long fields, for example, instead of foo.bar.baz, show f.b.baz',
},
'truncate:maxHeight': {
value: 115,
description: 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation.'
},
'indexPattern:fieldMapping:lookBack': {
value: 5,
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
'patterns from which to query the field mapping.'
}
};
};
});
});

View file

@ -43,6 +43,7 @@ define(function (require) {
// the id of the document
self.id = config.id || void 0;
self.defaults = config.defaults;
/**
* Asynchronously initialize this object - will only run
@ -119,20 +120,7 @@ define(function (require) {
_.assign(self, self._source);
return Promise.try(function () {
// if we have a searchSource, set it's state based on the searchSourceJSON field
if (self.searchSource) {
var state = {};
try {
state = JSON.parse(meta.searchSourceJSON);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
parseSearchSource(meta.searchSourceJSON);
})
.then(hydrateIndexPattern)
.then(function () {
@ -153,6 +141,23 @@ define(function (require) {
});
});
function parseSearchSource(searchSourceJson) {
if (!self.searchSource) return;
// if we have a searchSource, set its state based on the searchSourceJSON field
var state = {};
try {
state = JSON.parse(searchSourceJson);
} catch (e) {}
var oldState = self.searchSource.toJSON();
var fnProps = _.transform(oldState, function (dynamic, val, name) {
if (_.isFunction(val)) dynamic[name] = val;
}, {});
self.searchSource.set(_.defaults(state, fnProps));
}
/**
* After creation or fetching from ES, ensure that the searchSources index indexPattern
* is an bonafide IndexPattern object.
@ -181,14 +186,12 @@ define(function (require) {
});
}
/**
* Save this object
* Serialize this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
* @return {Object}
*/
self.save = function () {
self.serialize = function () {
var body = {};
_.forOwn(mapping, function (fieldMapping, fieldName) {
@ -205,6 +208,18 @@ define(function (require) {
};
}
return body;
};
/**
* Save this object
*
* @return {Promise}
* @resolved {String} - The id of the doc
*/
self.save = function () {
var body = self.serialize();
// Slugify the object id
self.id = slugifyId(self.id);
@ -229,7 +244,7 @@ define(function (require) {
return docSource.doCreate(source)
.then(finish)
.catch(function (err) {
var confirmMessage = 'Are you sure you want to overwrite this?';
var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?';
if (_.deepGet(err, 'origError.status') === 409 && window.confirm(confirmMessage)) {
return docSource.doIndex(source).then(finish);
}
@ -264,7 +279,6 @@ define(function (require) {
});
});
};
}
return SavedObject;

View file

@ -1,6 +1,6 @@
<th width="1%"></th>
<th ng-if="indexPattern.timeFieldName">
<span ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time">Time <i ng-class="headerClass(indexPattern.timeFieldName)"></i></span>
<span>Time <i ng-class="headerClass(indexPattern.timeFieldName)" ng-click="sort(indexPattern.timeFieldName)" tooltip="Sort by time"></i></span>
</th>
<th ng-repeat="name in columns">
<span class="table-header-name">
@ -11,4 +11,4 @@
<i ng-click="moveLeft(name)" class="fa fa-angle-double-left" ng-show="!$first" tooltip="Move column to the left" tooltip-append-to-body="1"></i>
<i ng-click="moveRight(name)" class="fa fa-angle-double-right" ng-show="!$last" tooltip="Move column to the right" tooltip-append-to-body="1"></i>
</span>
</th>
</th>

View file

@ -12,7 +12,7 @@ define(function (require) {
require('filters/short_dots');
// guestimate at the minimum number of chars wide cells in the table should be
// guesstimate at the minimum number of chars wide cells in the table should be
var MIN_LINE_LENGTH = 20;
/**
@ -235,4 +235,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -5,16 +5,16 @@
</ul>
<div class="content">
<table class="table table-condensed" ng-show="mode == 'table'" bindonce>
<table class="table table-condensed" ng-show="mode == 'table'">
<tbody>
<tr ng-repeat="field in fields" bindonce>
<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 bo-if="mapping[field].filterable">
<span ng-if="mapping[field].filterable">
<i ng-click="filter(mapping[field], flattened[field], '+')"
tooltip="Filter for value"
tooltip-append-to-body="1"
@ -24,11 +24,11 @@
tooltip-append-to-body="1"
class="fa fa-search-minus"></i>
</span>
<span bo-if="!mapping[field].filterable" tooltip="Unindexed fields can not be searched">
<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 bo-if="columns">
<span ng-if="columns">
<i ng-click="toggleColumn(field)"
tooltip="Toggle column in table"
tooltip-append-to-body="1"
@ -37,21 +37,19 @@
</td>
<td>
<i bo-if="!mapping[field] && field[0] === '_'"
<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 bo-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
<i ng-if="!mapping[field] && field[0] !== '_' && !showArrayInObjectsWarning(doc, field)"
tooltip-placement="top"
tooltip="No cached mapping for this field. Refresh your mapping from the Settings > Indices page"
class="fa fa-warning text-color-warning ng-scope doc-viewer-no-mapping"></i>
<i bo-if="showArrayInObjectsWarning(doc, field)"
<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>
<span class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></span>
<div class="doc-viewer-value" ng-bind-html="(typeof(formatted[field]) === 'undefined' ? hit[field] : formatted[field]) | highlight : hit.highlight[field] | trustAsHtml"></div>
</td>
</tr>
</tbody>

View file

@ -21,7 +21,11 @@ define(function (require) {
key = keyPrefix + key;
if (flat[key] !== void 0) return;
if (fields[key] || !_.isPlainObject(val)) {
var hasValidMapping = (fields[key] && fields[key].type !== 'conflict');
var isValue = !_.isPlainObject(val);
if (hasValidMapping || isValue) {
flat[key] = val;
return;
}

View file

@ -51,7 +51,7 @@ define(function (require) {
promise = self.getIndicesForIndexPattern(indexPattern)
.then(function (existing) {
if (existing.matches.length === 0) throw new IndexPatternMissingIndices();
return existing.matches.slice(-5); // Grab the most recent 5
return existing.matches.slice(-config.get('indexPattern:fieldMapping:lookBack')); // Grab the most recent
});
}

View file

@ -0,0 +1,34 @@
<div
ng-repeat="value in numberListCntr.getList() track by $index"
class="form-group vis-editor-agg-form-row vis-editor-agg-form-row">
<input
ng-model="numberListCntr.getList()[$index]"
kbn-number-list-input
input-focus
class="form-control">
<button
ng-click="numberListCntr.remove($index, 1)"
class="btn btn-danger btn-xs"
type="button">
<i class="fa fa-times"></i>
</button>
</div>
<p ng-show="numberListCntr.invalidLength()" class="text-danger text-center">
You must specify at least one {{numberListCntr.getUnitName()}}
</p>
<p ng-show="numberListCntr.undefinedLength()" class="text-primary text-center">
<!-- be a bit more polite when the form is first init'd -->
Please specify at least one {{numberListCntr.getUnitName()}}
</p>
<button
ng-click="numberListCntr.add()"
type="button"
class="sidebar-item-button primary">
<i class="fa fa-plus"></i> Add {{numberListCntr.getUnitName()}}
</button>

View file

@ -0,0 +1,108 @@
define(function (require) {
var _ = require('lodash');
var parseRange = require('utils/range');
require('components/number_list/number_list_input');
require('modules')
.get('kibana')
.directive('kbnNumberList', function () {
return {
restrict: 'E',
template: require('text!components/number_list/number_list.html'),
controllerAs: 'numberListCntr',
require: 'ngModel',
controller: function ($scope, $attrs, $parse) {
var self = this;
// Called from the pre-link function once we have the controllers
self.init = function (modelCntr) {
self.modelCntr = modelCntr;
self.getList = function () {
return self.modelCntr.$modelValue;
};
self.getUnitName = _.partial($parse($attrs.unit), $scope);
var defaultRange = self.range = parseRange('[0,Infinity)');
$scope.$watch(function () {
return $attrs.range;
}, function (range, prev) {
if (!range) {
self.range = defaultRange;
return;
}
try {
self.range = parseRange(range);
} catch (e) {
throw new TypeError('Unable to parse range: ' + e.message);
}
});
/**
* Remove an item from list by index
* @param {number} index
* @return {undefined}
*/
self.remove = function (index) {
var list = self.getList();
if (!list) return;
list.splice(index, 1);
};
/**
* Add an item to the end of the list
* @return {undefined}
*/
self.add = function () {
var list = self.getList();
if (!list) return;
list.push(_.last(list) + 1);
};
/**
* Check to see if the list is too short.
*
* @return {Boolean}
*/
self.tooShort = function () {
return _.size(self.getList()) < 1;
};
/**
* Check to see if the list is too short, but simply
* because the user hasn't interacted with it yet
*
* @return {Boolean}
*/
self.undefinedLength = function () {
return self.tooShort() && (self.modelCntr.$untouched && self.modelCntr.$pristine);
};
/**
* Check to see if the list is too short
*
* @return {Boolean}
*/
self.invalidLength = function () {
return self.tooShort() && !self.undefinedLength();
};
$scope.$watchCollection(self.getList, function () {
self.modelCntr.$setValidity('numberListLength', !self.tooShort());
});
};
},
link: {
pre: function ($scope, $el, attrs, ngModelCntr) {
$scope.numberListCntr.init(ngModelCntr);
}
},
};
});
});

View file

@ -6,18 +6,21 @@ define(function (require) {
var INVALID = {}; // invalid flag
var FLOATABLE = /^[\d\.e\-\+]+$/i;
var VALIDATION_ERROR = 'numberListRangeAndOrder';
var DIRECTIVE_ATTR = 'kbn-number-list-input';
require('modules')
.get('kibana')
.directive('valuesList', function ($parse) {
.directive('kbnNumberListInput', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $el, attrs, ngModelController) {
require: ['ngModel', '^kbnNumberList'],
link: function ($scope, $el, attrs, controllers) {
var ngModelCntr = controllers[0];
var numberListCntr = controllers[1];
var $setModel = $parse(attrs.ngModel).assign;
var $repeater = $el.closest('[ng-repeat]');
var $listGetter = $parse(attrs.valuesList);
var $minValue = $parse(attrs.valuesListMin);
var $maxValue = $parse(attrs.valuesListMax);
var handlers = {
up: change(add, 1),
@ -29,14 +32,16 @@ define(function (require) {
tab: go('next'),
'shift-tab': go('prev'),
'shift-enter': numberListCntr.add,
backspace: removeIfEmpty,
delete: removeIfEmpty
};
function removeIfEmpty(event) {
if ($el.val() === '') {
if (!ngModelCntr.$viewValue) {
$get('prev').focus();
$scope.remove($scope.$index);
numberListCntr.remove($scope.$index);
event.preventDefault();
}
@ -44,7 +49,7 @@ define(function (require) {
}
function $get(dir) {
return $repeater[dir]().find('[values-list]');
return $repeater[dir]().find('[' + DIRECTIVE_ATTR + ']');
}
function go(dir) {
@ -88,7 +93,7 @@ define(function (require) {
function change(using, mod) {
return function () {
var str = String(ngModelController.$viewValue);
var str = String(ngModelCntr.$viewValue);
var val = parse(str);
if (val === INVALID) return;
@ -96,7 +101,7 @@ define(function (require) {
if (next === INVALID) return;
$el.val(next);
ngModelController.$setViewValue(next);
ngModelCntr.$setViewValue(next);
};
}
@ -117,17 +122,26 @@ define(function (require) {
});
function parse(viewValue) {
viewValue = String(viewValue || 0);
var num = viewValue.trim();
if (!FLOATABLE.test(num)) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
var num = viewValue;
var list = $listGetter($scope);
var min = list[$scope.$index - 1] || $minValue($scope);
var max = list[$scope.$index + 1] || $maxValue($scope);
if (typeof num !== 'number' || isNaN(num)) {
// parse non-numbers
num = String(viewValue || 0).trim();
if (!FLOATABLE.test(num)) return INVALID;
if (num <= min || num >= max) return INVALID;
num = parseFloat(num);
if (isNaN(num)) return INVALID;
}
var range = numberListCntr.range;
if (!range.within(num)) return INVALID;
if ($scope.$index > 0) {
var i = $scope.$index - 1;
var list = numberListCntr.getList();
var prev = list[i];
if (num <= prev) return INVALID;
}
return num;
}
@ -137,31 +151,35 @@ define(function (require) {
{
fn: $scope.$watchCollection,
get: function () {
return $listGetter($scope);
return numberListCntr.getList();
}
}
], function () {
var valid = parse(ngModelController.$viewValue) !== INVALID;
ngModelController.$setValidity('valuesList', valid);
var valid = parse(ngModelCntr.$viewValue) !== INVALID;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
});
function validate(then) {
return function (input) {
var value = parse(input);
var valid = value !== INVALID;
value = valid ? value : void 0;
ngModelController.$setValidity('valuesList', valid);
value = valid ? value : input;
ngModelCntr.$setValidity(VALIDATION_ERROR, valid);
then && then(input, value);
return value;
};
}
ngModelController.$parsers.push(validate());
ngModelController.$formatters.push(validate(function (input, value) {
ngModelCntr.$parsers.push(validate());
ngModelCntr.$formatters.push(validate(function (input, value) {
if (input !== value) $setModel($scope, value);
}));
if (parse(ngModelCntr.$viewValue) === INVALID) {
ngModelCntr.$setTouched();
}
}
};
});
});
});

View file

@ -1,19 +1,23 @@
define(function (require) {
var _ = require('lodash');
var modules = require('modules');
var urlParam = '_a';
function AppStateProvider(Private, $rootScope, getAppState) {
var State = Private(require('components/state_management/state'));
_(AppState).inherits(State);
function AppState(defaults) {
AppState.Super.call(this, '_a', defaults);
AppState.Super.call(this, urlParam, defaults);
getAppState._set(this);
}
// if the url param is missing, write it back
AppState.prototype._persistAcrossApps = false;
AppState.prototype.destroy = function () {
AppState.Super.prototype.destroy.call(this);
getAppState._set(null);
@ -26,13 +30,19 @@ define(function (require) {
.factory('AppState', function (Private) {
return Private(AppStateProvider);
})
.service('getAppState', function () {
.service('getAppState', function ($location) {
var currentAppState;
function get() {
return currentAppState;
}
// Checks to see if the appState might already exist, even if it hasn't been newed up
get.previouslyStored = function () {
var search = $location.search();
return search[urlParam] ? true : false;
};
get._set = function (current) {
currentAppState = current;
};

View file

@ -43,7 +43,8 @@ define(function (require) {
if (self.shouldAutoReload(next, prev)) {
var appState = getAppState();
appState.destroy();
if (appState) appState.destroy();
reloading = $rootScope.$on('$locationChangeSuccess', function () {
// call the "unlisten" function returned by $on
reloading();

View file

@ -102,6 +102,14 @@ define(function (require) {
}
};
Vis.prototype.hasSchemaAgg = function (schemaName, aggTypeName) {
var aggs = this.aggs.bySchemaName[schemaName] || [];
return aggs.some(function (agg) {
if (!agg.type || !agg.type.name) return false;
return agg.type.name === aggTypeName;
});
};
return Vis;
};
});

View file

@ -133,7 +133,7 @@ define(function (require) {
var events = self.events ? self.events.eventResponse(d, i) : d;
return render(tooltipFormatter(events));
})
.on('mouseout.tip', function () {
.on('mouseleave.tip', function () {
render();
});
});

View file

@ -0,0 +1,15 @@
define(function (require) {
var d3 = require('d3');
/**
* Creates a string based on the hex color passed in
*
* @method dataLabel
* @param d {Object} object to wrap in d3.select
* @returns {string} label value
*/
function dataLabel(selection, label) {
d3.select(selection).attr('data-label', label);
}
return dataLabel;
});

View file

@ -512,30 +512,24 @@ define(function (require) {
};
/**
* Checks whether all pie slices have zero values.
* If so, an error is thrown.
* Removes zeros from pie chart data
* @param slices
* @returns {*}
*/
Data.prototype._validatePieData = function () {
var visData = this.getVisData();
Data.prototype._removeZeroSlices = function (slices) {
var self = this;
visData.forEach(function (chartData) {
chartData.slices = (function withoutZeroSlices(slices) {
if (!slices.children) return slices;
if (!slices.children) return slices;
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(withoutZeroSlices(child));
}
return children;
}, []);
return slices;
}(chartData.slices));
if (chartData.slices.children.length === 0) {
throw new errors.PieContainsAllZeros();
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(self._removeZeroSlices(child));
}
});
return children;
}, []);
return slices;
};
/**
@ -547,12 +541,12 @@ define(function (require) {
*/
Data.prototype.pieNames = function () {
var self = this;
var data = this.getVisData();
var names = [];
this._validatePieData();
_.forEach(this.getVisData(), function (obj) {
_.forEach(data, function (obj) {
var columns = obj.raw ? obj.raw.columns : undefined;
obj.slices = self._removeZeroSlices(obj.slices);
_.forEach(self.getNames(obj, columns), function (name) {
names.push(name);

View file

@ -227,14 +227,15 @@ define(function (require) {
* @method highlightLegend
*/
Dispatch.prototype.highlightLegend = function (element) {
var classList = d3.select(this).node().classList;
var liClass = d3.select(this).node().classList[1];
var label = this.getAttribute('data-label');
if (!label) return;
d3.select(element)
.select('.legend-ul')
.selectAll('li.color')
.filter(function (d, i) {
return d3.select(this).node().classList[1] !== liClass;
return this.getAttribute('data-label') !== label;
})
.classed('blur_shape', true);
};

View file

@ -77,6 +77,9 @@ define(function (require) {
Handler.prototype.render = function () {
var self = this;
var charts = this.charts = [];
var selection = d3.select(this.el);
selection.selectAll('*').remove();
this._validateData();
this.renderArray.forEach(function (property) {
@ -86,8 +89,7 @@ define(function (require) {
});
// render the chart(s)
d3.select(this.el)
.selectAll('.chart')
selection.selectAll('.chart')
.each(function (chartData) {
var chart = new self.ChartClass(self, this, chartData);
var enabledEvents;

View file

@ -2,6 +2,7 @@ define(function (require) {
return function LegendFactory(d3) {
var _ = require('lodash');
var legendHeaderTemplate = _.template(require('text!components/vislib/partials/legend_header.html'));
var dataLabel = require('components/vislib/lib/_data_label');
require('css!components/vislib/styles/main');
@ -76,27 +77,24 @@ define(function (require) {
.data(arrOfLabels)
.enter()
.append('li')
.attr('class', function (d) {
return 'color ' + self.colorToClass(args.color(d));
})
.attr('class', 'color')
.each(self._addIdentifier)
.html(function (d) {
return '<i class="fa fa-circle dots" style="color:' + args.color(d) + '"></i>' + d;
});
};
/**
* Creates a class name based on the hexColor assigned to each label
* Append the data label to the element
*
* @method colorToClass
* @param hexColor {String} Label
* @returns {string} CSS class name
* @method _addIdentifier
* @param label {string} label to use
*/
Legend.prototype.colorToClass = function (hexColor) {
if (hexColor) {
return 'c' + hexColor.replace(/[#]/g, '');
}
Legend.prototype._addIdentifier = function (label) {
dataLabel(this, label);
};
/**
* Renders legend
*
@ -134,35 +132,20 @@ define(function (require) {
});
legendDiv.select('.legend-ul').selectAll('li')
.on('mouseover', function (d) {
var liClass = self.colorToClass(self.color(d));
.on('mouseover', function (label) {
var charts = visEl.selectAll('.chart');
// legend
legendDiv.selectAll('li')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
return this.getAttribute('data-label') !== label.toString();
})
.classed('blur_shape', true);
// lines/area
charts.selectAll('.color')
// all data-label attribute
charts.selectAll('[data-label]')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// circles
charts.selectAll('.line circle')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
})
.classed('blur_shape', true);
// pie slices
charts.selectAll('.slice')
.filter(function (d) {
return d3.select(this).node().classList[1] !== liClass;
return this.getAttribute('data-label') !== label.toString();
})
.classed('blur_shape', true);
@ -182,16 +165,8 @@ define(function (require) {
legendDiv.selectAll('li')
.classed('blur_shape', false);
// lines/areas
charts.selectAll('.color')
.classed('blur_shape', false);
// circles
charts.selectAll('.line circle')
.classed('blur_shape', false);
// pie slices
charts.selectAll('.slice')
// all data-label attribute
charts.selectAll('[data-label]')
.classed('blur_shape', false);
var eventEl = d3.select(this);

View file

@ -254,7 +254,7 @@ define(function (require) {
div = d3.select(this);
width = parentWidth / n;
height = $(this).height();
height = $(this.parentElement).height();
self.validateWidthandHeight(width, height);

View file

@ -148,7 +148,7 @@
.x-axis-div-wrapper {
.display(flex);
.flex-direction(row);
min-height: 15px;
min-height: 20px;
min-width: 0;
}

View file

@ -49,6 +49,11 @@ define(function (require) {
throw new Error('No valid data!');
}
if (this.handler) {
this.data = null;
this._runOnHandler('destroy');
}
this.data = data;
this.handler = handlerTypes[chartType](this) || handlerTypes.column(this);
this._runOnHandler('render');
@ -100,10 +105,14 @@ define(function (require) {
* @method destroy
*/
Vis.prototype.destroy = function () {
var selection = d3.select(this.el).select('.vis-wrapper');
this.resizeChecker.off('resize', this.resize);
this.resizeChecker.destroy();
if (this.handler) this._runOnHandler('destroy');
d3.select(this.el).selectAll('*').remove();
selection.remove();
selection = null;
};
/**

View file

@ -1,10 +1,11 @@
define(function (require) {
return function ChartBaseClass(d3, Private) {
var _ = require('lodash');
var errors = require('errors');
var Legend = Private(require('components/vislib/lib/legend'));
var Dispatch = Private(require('components/vislib/lib/dispatch'));
var Tooltip = Private(require('components/vislib/components/tooltip/tooltip'));
var dataLabel = require('components/vislib/lib/_data_label');
/**
* The Base Class for all visualizations.
@ -44,18 +45,25 @@ define(function (require) {
* @returns {HTMLElement} Contains the D3 chart
*/
Chart.prototype.render = function () {
return d3.select(this.chartEl).call(this.draw());
var selection = d3.select(this.chartEl);
selection.selectAll('*').remove();
selection.call(this.draw());
};
/**
* Returns a CSS class name
* Append the data label to the element
*
* @method colorToClass
* @param label {String} Data point label
* @returns {String} CSS class name
* @method _addIdentifier
* @param selection {Object} d3 select object
*/
Chart.prototype.colorToClass = function (label) {
return Legend.prototype.colorToClass.call(null, label);
Chart.prototype._addIdentifier = function (selection, labelProp) {
labelProp = labelProp || 'label';
selection.each(function (datum) {
var label = datum[0] ? datum[0][labelProp] : datum[labelProp];
if (label == null) return;
dataLabel(this, label);
});
};
/**
@ -64,7 +72,10 @@ define(function (require) {
* @method destroy
*/
Chart.prototype.destroy = function () {
d3.select(this.chartEl).selectAll('*').remove();
var selection = d3.select(this.chartEl);
selection.remove();
selection = null;
};
return Chart;

View file

@ -4,6 +4,7 @@ define(function (require) {
var $ = require('jquery');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
var errors = require('errors');
require('css!components/vislib/styles/main');
@ -82,22 +83,18 @@ define(function (require) {
})
.interpolate(interpolate);
var layer;
var path;
// Data layers
layer = svg.selectAll('.layer')
var layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.enter()
.append('g')
.attr('class', function (d, i) {
return i;
return 'pathgroup ' + i;
});
// Append path
path = layer.append('path')
.attr('class', function (d) {
return 'color ' + self.colorToClass(color(d[0].label));
})
var path = layer.append('path')
.call(this._addIdentifier)
.style('fill', function (d) {
return color(d[0].label);
})
@ -151,7 +148,7 @@ define(function (require) {
var yScale = this.handler.yAxis.yScale;
var ordered = this.handler.data.get('ordered');
var circleRadius = 12;
var circleStrokeWidth = 1;
var circleStrokeWidth = 0;
var tooltip = this.tooltip;
var isTooltip = this._attr.addTooltip;
var isOverlapping = this.isOverlapping;
@ -180,9 +177,7 @@ define(function (require) {
circles
.enter()
.append('circle')
.attr('class', function circleClass(d) {
return d.label + ' ' + self.colorToClass(color(d.label));
})
.call(this._addIdentifier)
.attr('stroke', function strokeColor(d) {
return color(d.label);
})
@ -272,6 +267,9 @@ define(function (require) {
var yScale = this.handler.yAxis.yScale;
var minWidth = 20;
var minHeight = 20;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -289,6 +287,10 @@ define(function (require) {
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -339,6 +341,10 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -5,6 +5,7 @@ define(function (require) {
var moment = require('moment');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
var errors = require('errors');
require('css!components/vislib/styles/main');
@ -68,9 +69,7 @@ define(function (require) {
bars
.enter()
.append('rect')
.attr('class', function (d) {
return 'color ' + self.colorToClass(color(d.label));
})
.call(this._addIdentifier)
.attr('fill', function (d) {
return color(d.label);
});
@ -271,8 +270,12 @@ define(function (require) {
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var xScale = this.handler.xAxis.xScale;
var minWidth = 20;
var minHeight = 20;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -287,6 +290,10 @@ define(function (require) {
width = elWidth;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -327,6 +334,10 @@ define(function (require) {
.style('stroke-width', 1);
}
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -5,6 +5,7 @@ define(function (require) {
var errors = require('errors');
var PointSeriesChart = Private(require('components/vislib/visualizations/_point_series_chart'));
var TimeMarker = Private(require('components/vislib/visualizations/time_marker'));
require('css!components/vislib/styles/main');
/**
@ -144,9 +145,8 @@ define(function (require) {
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', colorCircle)
.attr('class', function circleClass(d) {
return 'circle-decoration ' + self.colorToClass(color(d.label));
});
.attr('class', 'circle-decoration')
.call(this._addIdentifier);
circles
.enter()
@ -155,9 +155,8 @@ define(function (require) {
.attr('cx', cx)
.attr('cy', cy)
.attr('fill', 'transparent')
.attr('class', function circleClass(d) {
return 'circle ' + self.colorToClass(color(d.label));
})
.attr('class', 'circle')
.call(this._addIdentifier)
.attr('stroke', cColor)
.attr('stroke-width', 0);
@ -202,12 +201,10 @@ define(function (require) {
.data(data)
.enter()
.append('g')
.attr('class', 'lines');
.attr('class', 'pathgroup lines');
lines.append('path')
.attr('class', function lineClass(d) {
return 'color ' + self.colorToClass(color(d.label));
})
.call(this._addIdentifier)
.attr('d', function lineD(d) {
return line(d.values);
})
@ -262,10 +259,14 @@ define(function (require) {
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var xScale = this.handler.xAxis.xScale;
var minWidth = 20;
var minHeight = 20;
var startLineX = 0;
var lineStrokeWidth = 1;
var addTimeMarker = this._attr.addTimeMarker;
var times = this._attr.times || [];
var timeMarker;
var div;
var svg;
var width;
@ -292,6 +293,10 @@ define(function (require) {
width = elWidth - margin.left - margin.right;
height = elHeight - margin.top - margin.bottom;
if (addTimeMarker) {
timeMarker = new TimeMarker(times, xScale, height);
}
if (width < minWidth || height < minHeight) {
throw new errors.ContainerTooSmall();
}
@ -335,6 +340,10 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
if (addTimeMarker) {
timeMarker.render(svg);
}
return svg;
});
};

View file

@ -24,11 +24,26 @@ define(function (require) {
}
PieChart.Super.apply(this, arguments);
var charts = this.handler.data.getVisData();
this._validatePieData(charts);
this._attr = _.defaults(handler._attr || {}, {
isDonut: handler._attr.isDonut || false
});
}
/**
* Checks whether pie slices have all zero values.
* If so, an error is thrown.
*/
PieChart.prototype._validatePieData = function (charts) {
var isAllZeros = charts.every(function (chart) {
return chart.slices.children.length === 0;
});
if (isAllZeros) { throw new errors.PieContainsAllZeros(); }
};
/**
* Adds Events to SVG paths
*
@ -83,10 +98,19 @@ define(function (require) {
* @returns {D3.Selection} SVG with paths attached
*/
PieChart.prototype.addPath = function (width, height, svg, slices) {
var self = this;
var marginFactor = 0.95;
var isDonut = this._attr.isDonut;
var isDonut = self._attr.isDonut;
var radius = (Math.min(width, height) / 2) * marginFactor;
var color = this.handler.data.getPieColorFunc();
var color = self.handler.data.getPieColorFunc();
var tooltip = self.tooltip;
var isTooltip = self._attr.addTooltip;
var format = function (d, label) {
var formatter = d.aggConfig ? d.aggConfig.fieldFormatter() : String;
return formatter(label);
};
var partition = d3.layout.partition()
.sort(null)
.value(function (d) {
@ -115,16 +139,8 @@ define(function (require) {
.outerRadius(function (d) {
return Math.max(0, y(d.y + d.dy));
});
var tooltip = this.tooltip;
var isTooltip = this._attr.addTooltip;
var self = this;
var path;
var format = function (d, label) {
var formatter = d.aggConfig ? d.aggConfig.fieldFormatter() : String;
return formatter(label);
};
path = svg
var path = svg
.datum(slices)
.selectAll('path')
.data(partition.nodes)
@ -133,8 +149,9 @@ define(function (require) {
.attr('d', arc)
.attr('class', function (d) {
if (d.depth === 0) { return; }
return 'slice ' + self.colorToClass(color(format(d, d.name)));
return 'slice';
})
.call(self._addIdentifier, 'name')
.style('stroke', '#fff')
.style('fill', function (d) {
if (d.depth === 0) { return 'none'; }
@ -148,6 +165,15 @@ define(function (require) {
return path;
};
PieChart.prototype._validateContainerSize = function (width, height) {
var minWidth = 20;
var minHeight = 20;
if (width <= minWidth || height <= minHeight) {
throw new errors.ContainerTooSmall();
}
};
/**
* Renders d3 visualization
*
@ -160,17 +186,15 @@ define(function (require) {
return function (selection) {
selection.each(function (data) {
var slices = data.slices;
var el = this;
var div = d3.select(el);
var width = $(el).width();
var height = $(el).height();
var minWidth = 20;
var minHeight = 20;
var div = d3.select(this);
var width = $(this).width();
var height = $(this).height();
var path;
if (width <= minWidth || height <= minHeight) {
throw new errors.ContainerTooSmall();
}
if (!slices.children.length) return;
self.convertToPercentage(slices);
self._validateContainerSize(width, height);
var svg = div.append('svg')
.attr('width', width)
@ -178,7 +202,6 @@ define(function (require) {
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
self.convertToPercentage(slices);
path = self.addPath(width, height, svg, slices);
self.addPathEvents(path);

View file

@ -178,34 +178,13 @@ define(function (require) {
} else if (this._attr.mapType === 'Shaded Geohash Grid') {
featureLayer = this.shadedGeohashGrid(map, mapData);
} else {
featureLayer = this.pinMarkers(mapData);
featureLayer = this.scaledCircleMarkers(map, mapData);
}
}
return featureLayer;
};
/**
* Type of data overlay for map:
* creates featurelayer from mapData (geoJson)
* with default leaflet pin markers
*
* @method pinMarkers
* @param mapData {Object}
* @return {Leaflet object} featureLayer
*/
TileMap.prototype.pinMarkers = function (mapData) {
var self = this;
var featureLayer = L.geoJson(mapData, {
onEachFeature: function (feature, layer) {
self.bindPopup(feature, layer);
}
});
return featureLayer;
};
/**
* Type of data overlay for map:
* creates featurelayer from mapData (geoJson)

View file

@ -0,0 +1,74 @@
define(function (require) {
var datemath = require('utils/datemath');
return function TimeMarkerFactory(d3) {
function TimeMarker(times, xScale, height) {
if (!(this instanceof TimeMarker)) {
return new TimeMarker(times, xScale, height);
}
var currentTimeArr = [{
'time': new Date().getTime(),
'class': 'time-marker',
'color': '#c80000',
'opacity': 0.3,
'width': 2
}];
this.xScale = xScale;
this.height = height;
this.times = (times.length) ? times.map(function (d) {
return {
'time': datemath.parse(d.time),
'class': d.class || 'time-marker',
'color': d.color || '#c80000',
'opacity': d.opacity || 0.3,
'width': d.width || 2
};
}) : currentTimeArr;
}
TimeMarker.prototype._isTimeBasedChart = function (selection) {
var data = selection.data();
return data.every(function (datum) {
return (datum.ordered && datum.ordered.date);
});
};
TimeMarker.prototype.render = function (selection) {
var self = this;
// return if not time based chart
if (!self._isTimeBasedChart(selection)) return;
selection.each(function () {
d3.select(this).selectAll('time-marker')
.data(self.times)
.enter().append('line')
.attr('class', function (d) {
return d.class;
})
.attr('pointer-events', 'none')
.attr('stroke', function (d) {
return d.color;
})
.attr('stroke-width', function (d) {
return d.width;
})
.attr('stroke-opacity', function (d) {
return d.opacity;
})
.attr('x1', function (d) {
return self.xScale(d.time);
})
.attr('x2', function (d) {
return self.xScale(d.time);
})
.attr('y1', self.height)
.attr('y2', self.xScale.range()[0]);
});
};
return TimeMarker;
};
});

View file

@ -84,7 +84,6 @@ visualize-spy {
&-tab {
margin: 0px auto;
margin-top: -1px;
border: 1px solid #ecf0f1;
border-top: 0px;
border-width: 0px 1px 1px 1px;
@ -92,10 +91,14 @@ visualize-spy {
border-bottom-right-radius: @border-radius-base;
width: 50px;
background: @body-bg;
text-align: center;
text-align: center;
&:hover {
background-color: @gray-lighter;
}
}
i {
padding: 0 10px;
}

View file

@ -0,0 +1,32 @@
define(function (require) {
var module = require('modules').get('kibana');
var $ = require('jquery');
module.directive('fileUpload', function ($parse) {
return {
restrict: 'A',
link: function ($scope, $elem, attrs) {
var onUpload = $parse(attrs.fileUpload);
var $fileInput = $('<input type="file" style="opacity: 0" id="testfile" />');
$elem.after($fileInput);
$fileInput.on('change', function (e) {
var reader = new FileReader();
reader.onload = function (e) {
$scope.$apply(function () {
onUpload($scope, {fileContents: e.target.result});
});
};
var target = e.srcElement || e.target;
if (target && target.files && target.files.length) reader.readAsText(target.files[0]);
});
$elem.on('click', function (e) {
$fileInput.trigger('click');
});
}
};
});
});

View file

@ -1,6 +1,6 @@
<div class="pagination-other-pages">
<ul class="pagination-other-pages-list pagination-sm" ng-if="page.count > 1">
<li ng-class="{disabled: page.first}">
<li ng-hide="page.first">
<a ng-click="paginate.goToPage(page.prev)">«</a>
</li>
@ -18,7 +18,7 @@
<a ng-click="paginate.goToPage(page.count)">...{{page.count}}</a>
</li>
<li ng-class="{disabled: page.last}">
<li ng-hide="page.last">
<a ng-click="paginate.goToPage(page.next)">»</a>
</li>
</ul>
@ -30,4 +30,4 @@
<select ng-model="paginate.perPage" ng-options="opt.value as opt.title for opt in paginate.sizeOptions">
</select>
</div>
</form>
</form>

View file

@ -49,7 +49,7 @@ define(function (require) {
app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter, kbnUrl) {
return {
controller: function ($scope, $route, $routeParams, $location, configFile, Private) {
controller: function ($scope, $route, $routeParams, $location, configFile, Private, getAppState) {
var filterBarWatchFilters = Private(require('components/filter_bar/lib/watchFilters'));
var notify = new Notifier({
@ -57,6 +57,12 @@ define(function (require) {
});
var dash = $scope.dash = $route.current.locals.dash;
if (dash.timeRestore && dash.timeTo && dash.timeFrom && !getAppState.previouslyStored()) {
timefilter.time.to = dash.timeTo;
timefilter.time.from = dash.timeFrom;
}
$scope.$on('$destroy', dash.destroy);
var matchQueryFilter = function (filter) {
@ -135,6 +141,8 @@ define(function (require) {
$state.title = dash.id = dash.title;
$state.save();
dash.panelsJSON = angular.toJson($state.panels);
dash.timeFrom = dash.timeRestore ? timefilter.time.from : undefined;
dash.timeTo = dash.timeRestore ? timefilter.time.to : undefined;
dash.save()
.then(function (id) {

View file

@ -3,5 +3,11 @@
<label for="dashboardTitle">Save As</label>
<input id="dashboardTitle" type="text" ng-model="opts.dashboard.title" class="form-control" placeholder="Dashboard title" input-focus="select">
</div>
<div class="form-group">
<label>
<input type="checkbox" ng-model="opts.dashboard.timeRestore" ng-checked="opts.dashboard.timeRestore">
Store time with dashboard <i class="fa fa-info-circle ng-scope" tooltip="Change the time filter to the currently selected time each time this dashboard is loaded" tooltip-placement="" tooltip-popup-delay="250"></i>
</label>
</div>
<button type="submit" ng-disabled="!opts.dashboard.title" class="btn btn-primary" aria-label="Save dashboard">Save</button>
</form>

View file

@ -1,6 +1,7 @@
define(function (require) {
var module = require('modules').get('app/dashboard');
var _ = require('lodash');
var moment = require('moment');
// Used only by the savedDashboards service, usually no reason to change this
module.factory('SavedDashboard', function (courier) {
@ -23,7 +24,10 @@ define(function (require) {
hits: 'integer',
description: 'string',
panelsJSON: 'string',
version: 'integer'
version: 'integer',
timeRestore: 'boolean',
timeTo: 'string',
timeFrom: 'string'
},
// defeult values to assign to the doc
@ -32,7 +36,10 @@ define(function (require) {
hits: 0,
description: '',
panelsJSON: '[]',
version: 1
version: 1,
timeRestore: false,
timeTo: undefined,
timeFrom: undefined
},
searchSource: true,

View file

@ -35,7 +35,8 @@ define(function (require) {
if (field.analyzed && field.type === 'string') {
warnings.push('This is an analyzed string field.' +
' Analyzed strings are highly unique and can use a lot of memory to visualize.');
' Analyzed strings are highly unique and can use a lot of memory to visualize.' +
' Values such as foo-bar will be broken into foo and bar.');
}
if (!field.indexed) {
@ -94,4 +95,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -75,6 +75,17 @@ define(function (require) {
location: 'Discover'
});
$scope.intervalOptions = Private(require('components/agg_types/buckets/_interval_options'));
$scope.showInterval = false;
$scope.intervalEnabled = function (interval) {
return interval.val !== 'custom';
};
$scope.toggleInterval = function () {
$scope.showInterval = !$scope.showInterval;
};
// config panel templates
$scope.configTemplate = new ConfigTemplate({
load: require('text!plugins/discover/partials/load_search.html'),
@ -166,6 +177,23 @@ define(function (require) {
timefilter.enabled = !!timefield;
});
$scope.$watch('state.interval', function (interval, oldInterval) {
if (interval !== oldInterval && interval === 'auto') {
$scope.showInterval = false;
}
$scope.fetch();
});
$scope.$watch('vis.aggs', function (aggs) {
var buckets = $scope.vis.aggs.bySchemaGroup.buckets;
if (buckets && buckets.length === 1) {
$scope.intervalName = 'by ' + buckets[0].buckets.getInterval().description;
} else {
$scope.intervalName = 'auto';
}
});
$scope.$watchMulti([
'rows',
'fetchStatus'
@ -394,7 +422,8 @@ define(function (require) {
.highlight({
pre_tags: [highlightTags.pre],
post_tags: [highlightTags.post],
fields: {'*': {}}
fields: {'*': {}},
fragment_size: 2147483647 // Limit of an integer.
})
.set('filter', $state.filters || []);
});
@ -425,22 +454,37 @@ define(function (require) {
if (!$scope.opts.timefield) return Promise.resolve();
if (loadingVis) return loadingVis;
var visStateAggs = [
{
type: 'count',
schema: 'metric'
},
{
type: 'date_histogram',
schema: 'segment',
params: {
field: $scope.opts.timefield,
interval: $state.interval,
min_doc_count: 0
}
}
];
// we shouldn't have a vis, delete it
if (!$scope.opts.timefield && $scope.vis) {
$scope.vis.destroy();
$scope.searchSource.set('aggs', undefined);
delete $scope.vis;
// we have a vis, just modify the aggs
if ($scope.vis) {
var visState = $scope.vis.getState();
visState.aggs = visStateAggs;
$scope.vis.setState(visState);
return Promise.resolve($scope.vis);
}
// we shouldn't have one, or already do, return whatever we already have
if (!$scope.opts.timefield || $scope.vis) return Promise.resolve($scope.vis);
// TODO: a legit way to update the index pattern
$scope.vis = new Vis($scope.indexPattern, {
type: 'histogram',
params: {
addLegend: false,
addTimeMarker: true
},
listeners: {
click: function (e) {
@ -451,21 +495,7 @@ define(function (require) {
},
brush: brushEvent
},
aggs: [
{
type: 'count',
schema: 'metric'
},
{
type: 'date_histogram',
schema: 'segment',
params: {
field: $scope.opts.timefield,
interval: 'auto',
min_doc_count: 0
}
}
]
aggs: visStateAggs
});
$scope.searchSource.aggs(function () {

View file

@ -79,6 +79,7 @@
<span bindonce bo-bind="opts.savedSearch.title"></span>
<i aria-label="Reload saved query" tooltip="Reload saved query" ng-click="resetQuery();" class="fa fa-undo small"></i>
</span>
<strong class="discover-info-hits">{{(hits || 0) | number:0}}</strong>
<ng-pluralize count="hits" when="{'1':'hit', 'other':'hits'}"></ng-pluralize>
</div>
@ -113,7 +114,7 @@
<h3>Refine your query</h3>
<p>
The search bar at the top uses Elasticsearch's support for Lucene Query String syntax. Let's say we're searching web server logs that have been parsed into a few fields.
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>
@ -149,17 +150,30 @@
<!-- result -->
<div class="results" ng-show="resultState === 'ready'">
<div class="discover-timechart" ng-if="opts.timefield">
<center class="small">
<span tooltip="To change the time, click the clock icon in the navigation bar">{{timeRange.from | moment}} - {{timeRange.to | moment}}</span>
<!-- TODO: Currently no way to apply this setting to the visualization -->
<!-- <select
class="form-control"
ng-model="state.interval"
ng-options="interval as interval for interval in intervalOptions"
ng-change="setupVisualization();fetch()"
>
</select> -->
</center>
<header>
<center class="small">
<span tooltip="To change the time, click the clock icon in the navigation bar">{{timeRange.from | moment}} - {{timeRange.to | moment}}</span>
&mdash;
<span class="results-interval" ng-hide="showInterval">
<a
ng-click="toggleInterval()">
{{ intervalName }}
</a>
</span>
<span ng-show="showInterval" class="results-interval form-inline">
<select
class="form-control"
ng-model="state.interval"
ng-options="interval.val as interval.display for interval in intervalOptions | filter: intervalEnabled"
>
</select>
</span>
</center>
</header>
<visualize ng-if="vis && rows.length != 0" vis="vis" es-resp="mergedEsResp" search-source="searchSource"></visualize>
</div>

View file

@ -21,6 +21,11 @@
display: block;
position: relative;
header {
min-height: @input-height-base + 8px;
padding: 4px 0;
}
visualize {
height: 200px;
max-height: 600px;
@ -248,4 +253,15 @@ disc-field-chooser {
margin: -20px 0 10px 0;
text-align: center;
}
&-interval {
a {
text-decoration: underline;;
}
select {
display: inline-block;
width: auto;
}
}
}

View file

@ -23,7 +23,7 @@
</ul>
<ul class="nav navbar-nav navbar-right navbar-timepicker">
<li ng-show="httpActive.length" class="navbar-text">
<li ng-show="httpActive.length" class="navbar-text hidden-xs">
<div class="spinner"></div>
</li>

View file

@ -8,11 +8,15 @@
<span class="smaller">{{conf.description}}</span>
</td>
<td class="value">
<!-- Settings editors -->
<form
name="forms.configEdit"
ng-if="conf.editting"
ng-submit="save(conf)"
role="form">
<input
ng-show="conf.normal"
ng-model="conf.unsavedValue"
@ -20,14 +24,7 @@
placeholder="{{conf.value || conf.defVal}}"
type="text"
class="form-control">
<input
ng-show="conf.array"
ng-list=","
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
placeholder="{{(conf.value || conf.defVal).join(', ')}}"
type="text"
class="form-control">
<textarea
ng-if="conf.json"
type="text"
@ -36,16 +33,38 @@
ng-keyup="maybeCancel($event, conf)"
validate-json
></textarea>
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>
<small ng-show="forms.configEdit.$error.jsonInput">Invalid JSON syntax</small>
<input
ng-show="conf.array"
ng-list=","
ng-model="conf.unsavedValue"
ng-keyup="maybeCancel($event, conf)"
placeholder="{{(conf.value || conf.defVal).join(', ')}}"
type="text"
class="form-control">
<input
ng-show="conf.bool"
ng-model="conf.unsavedValue"
type="checkbox"
class="form-control">
<select
ng-show="conf.select"
name="conf.name"
ng-model="conf.unsavedValue"
ng-options="option as option for option in conf.options"
class="form-control">
</select>
</form>
<span ng-if="!conf.editting && (conf.normal || conf.json)">{{conf.value || conf.defVal}}</span>
<span ng-if="!conf.editting && conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
<span ng-if="!conf.editting && conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<!-- Setting display formats -->
<span ng-if="!conf.editting">
<span ng-show="(conf.normal || conf.json || conf.select)">{{conf.value || conf.defVal}}</span>
<span ng-show="conf.array">{{(conf.value || conf.defVal).join(', ')}}</span>
<span ng-show="conf.bool">{{conf.value === undefined ? conf.defVal : conf.value}}</span>
</span>
</td>
<td class="actions">

View file

@ -1,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var configDefaults = require('components/config/defaults');
require('modules').get('apps/settings')
.directive('advancedRow', function (config, Notifier, Private) {
@ -13,6 +12,7 @@ define(function (require) {
configs: '='
},
link: function ($scope) {
var configDefaults = Private(require('components/config/defaults'));
var notify = new Notifier();
var keyCodes = {
ESC: 27
@ -66,4 +66,4 @@ define(function (require) {
}
};
});
});
});

View file

@ -1,6 +1,5 @@
define(function (require) {
var _ = require('lodash');
var configDefaults = require('components/config/defaults');
var getValType = require('plugins/settings/sections/advanced/lib/get_val_type');
@ -16,11 +15,12 @@ define(function (require) {
return {
restrict: 'E',
link: function ($scope) {
var configDefaults = Private(require('components/config/defaults'));
var keyCodes = {
ESC: 27
};
var NAMED_EDITORS = ['json', 'array', 'boolean'];
var NAMED_EDITORS = ['json', 'array', 'boolean', 'select'];
var NORMAL_EDITOR = ['number', 'string', 'null', 'undefined'];
function getEditorType(conf) {
@ -42,11 +42,13 @@ define(function (require) {
defVal: def.value,
type: getValType(def, val),
description: def.description,
options: def.options,
value: val,
};
var editor = getEditorType(conf);
conf.json = editor === 'json';
conf.select = editor === 'select';
conf.bool = editor === 'boolean';
conf.array = editor === 'array';
conf.normal = editor === 'normal';
@ -68,4 +70,4 @@ define(function (require) {
display: 'Advanced',
url: '#/settings/advanced'
};
});
});

View file

@ -45,10 +45,10 @@
<i aria-hidden="true" class="fa-link fa"></i>
</a>
</p>
<div bo-if="indexPattern.timeFieldName && indexPattern.intervalName" class="alert alert-info">
<div ng-if="indexPattern.timeFieldName && indexPattern.intervalName" class="alert alert-info">
This index uses a <strong>Time-based index pattern</strong> which repeats <span bo-text="indexPattern.getInterval().display"></span>
</div>
<div bo-if="conflictFields.length" class="alert alert-warning">
<div ng-if="conflictFields.length" class="alert alert-warning">
<strong>Mapping conflict!</strong> {{conflictFields.length > 1 ? conflictFields.length : 'A'}} field{{conflictFields.length > 1 ? 's' : ''}} {{conflictFields.length > 1 ? 'are' : 'is'}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.
</div>
</div>

View file

@ -38,7 +38,9 @@ define(function (require) {
if (!tab) $scope.changeTab($scope.fieldTypes[0]);
});
$scope.conflictFields = _.filter($scope.indexPattern.fields, {type: 'conflict'});
$scope.$watchCollection('indexPattern.fields', function () {
$scope.conflictFields = _.filter($scope.indexPattern.fields, {type: 'conflict'});
});
$scope.refreshFields = function () {
$scope.indexPattern.refreshFields();
@ -73,4 +75,4 @@ define(function (require) {
return $scope.indexPattern.save();
};
});
});
});

View file

@ -4,7 +4,7 @@
<h5>
Index Patterns&nbsp;
<a
bo-if="edittingId"
ng-if="edittingId"
href="#/settings/indices"
class="btn btn-primary btn-xs"
aria-label="Add New">

View file

@ -22,11 +22,8 @@ define(function (require) {
template: require('text!plugins/settings/sections/indices/index.html'),
link: function ($scope) {
$scope.edittingId = $route.current.params.id;
$scope.defaultIndex = config.get('defaultIndex');
$rootScope.$on('change:config.defaultIndex', function () {
$scope.defaultIndex = config.get('defaultIndex');
});
config.$bind($scope, 'defaultIndex');
$scope.$watch('defaultIndex', function (defaultIndex) {
$scope.indexPatternList = _($route.current.locals.indexPatternIds)
.map(function (id) {
@ -50,4 +47,4 @@ define(function (require) {
display: 'Indices',
url: '#/settings/indices',
};
});
});

View file

@ -1,6 +1,10 @@
<kbn-settings-app section="objects">
<kbn-settings-objects class="container">
<h2>Edit Saved Objects</h2>
<div class="header">
<h2 class="title">Edit Saved Objects</h2>
<button class="btn btn-default controls" ng-click="exportAll()"><i aria-hidden="true" class="fa fa-download"></i> Export</button>
<button file-upload="importAll(fileContents)" class="btn btn-default controls" ng-click><i aria-hidden="true" class="fa fa-upload"></i> Import</button>
</div>
<p>
From here you can delete saved objects, such as saved searches. You can also edit the raw data of saved objects. Typically objects are only modified via their associated application, which is probably what you should use instead of this screen. Each tab is limited to 100 results. You can use the filter to find objects not in the default list.
</p>
@ -20,13 +24,16 @@
<div class="tab-content">
<div class="action-bar">
<label>
<input type="checkbox" ng-model="deleteAll">
<input type="checkbox" ng-checked="currentTab.data.length > 0 && selectedItems.length == currentTab.data.length" ng-click="toggleAll()" />
Select All
</label>
<a ng-disabled="!deleteAllBtn"
<a ng-disabled="selectedItems.length == 0"
confirm-click="bulkDelete()"
confirmation="Are you sure want to delete the selected {{service.title}}? This action is irreversible!"
class="delete-all btn btn-danger btn-xs" aria-label="Delete Selected"><i aria-hidden="true" class="fa fa-trash"></i> Delete Selected</a>
confirmation="Are you sure want to delete the selected {{currentTab.title}}? This action is irreversible!"
class="btn btn-xs btn-danger" aria-label="Delete"><i aria-hidden="true" class="fa fa-trash"></i> Delete</a>
<a ng-disabled="selectedItems.length == 0"
ng-click="bulkExport()"
class="btn btn-xs btn-default" aria-label="Export"><i aria-hidden="true" class="fa fa-download"></i> Export</a>
</div>
<div ng-repeat="service in services" ng-class="{ active: state.tab === service.title }" class="tab-pane">
<ul class="list-unstyled">
@ -51,8 +58,8 @@
<div class="pull-left">
<input
ng-click="item.checked = !item.checked; toggleDeleteBtn(service)"
ng-checked="item.checked"
ng-click="toggleItem(item)"
ng-checked="selectedItems.indexOf(item) >= 0"
type="checkbox" >
</div>

View file

@ -1,8 +1,12 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
var saveAs = require('file_saver');
var registry = require('plugins/settings/saved_object_registry');
var objectIndexHTML = require('text!plugins/settings/sections/objects/_objects.html');
require('directives/file_upload');
require('routes')
.when('/settings/objects', {
template: objectIndexHTML
@ -12,42 +16,52 @@ define(function (require) {
.directive('kbnSettingsObjects', function (config, Notifier, Private, kbnUrl) {
return {
restrict: 'E',
controller: function ($scope, $injector, $q, AppState) {
controller: function ($scope, $injector, $q, AppState, es) {
var notify = new Notifier({ location: 'Saved Objects' });
var $state = $scope.state = new AppState();
var resetCheckBoxes = function () {
$scope.deleteAll = false;
_.each($scope.services, function (service) {
_.each(service.data, function (item) {
item.checked = false;
});
});
};
$scope.currentTab = null;
$scope.selectedItems = [];
var getData = function (filter) {
var services = registry.all().map(function (obj) {
var service = $injector.get(obj.service);
return service.find(filter).then(function (data) {
return { service: obj.service, title: obj.title, data: data.hits, total: data.total };
return {
service: service,
serviceName: obj.service,
title: obj.title,
type: service.type,
data: data.hits,
total: data.total
};
});
});
$q.all(services).then(function (data) {
$scope.services = _.sortBy(data, 'title');
if (!$state.tab) {
$scope.changeTab($scope.services[0]);
}
var tab = $scope.services[0];
if ($state.tab) tab = _.find($scope.services, {title: $state.tab});
$scope.changeTab(tab);
});
};
$scope.$watch('deleteAll', function (checked) {
var service = _.find($scope.services, { title: $state.tab });
if (!service) return;
_.each(service.data, function (item) {
item.checked = checked;
});
$scope.toggleDeleteBtn(service);
});
$scope.toggleAll = function () {
if ($scope.selectedItems.length === $scope.currentTab.data.length) {
$scope.selectedItems.length = 0;
} else {
$scope.selectedItems = [].concat($scope.currentTab.data);
}
};
$scope.toggleItem = function (item) {
var i = $scope.selectedItems.indexOf(item);
if (i >= 0) {
$scope.selectedItems.splice(i, 1);
} else {
$scope.selectedItems.push(item);
}
};
$scope.open = function (item) {
kbnUrl.change(item.url.substr(1));
@ -55,43 +69,103 @@ define(function (require) {
$scope.edit = function (service, item) {
var params = {
service: service.service,
service: service.serviceName,
id: item.id
};
kbnUrl.change('/settings/objects/{{ service }}/{{ id }}', params);
};
$scope.toggleDeleteBtn = function (service) {
$scope.deleteAllBtn = _.some(service.data, { checked: true});
$scope.bulkDelete = function () {
$scope.currentTab.service.delete(_.pluck($scope.selectedItems, 'id')).then(refreshData);
};
$scope.bulkDelete = function () {
var serviceObj = _.find($scope.services, { title: $state.tab });
if (!serviceObj) return;
var service = $injector.get(serviceObj.service);
var ids = _(serviceObj.data)
.filter({ checked: true})
.pluck('id')
.value();
service.delete(ids).then(function (resp) {
serviceObj.data = _.filter(serviceObj.data, function (obj) {
return !obj.checked;
});
resetCheckBoxes();
$scope.bulkExport = function () {
var objs = $scope.selectedItems.map(_.partialRight(_.extend, {type: $scope.currentTab.type}));
retrieveAndExportDocs(objs);
};
$scope.exportAll = function () {
var objs = $scope.services.map(function (service) {
return service.data.map(_.partialRight(_.extend, {type: service.type}));
});
retrieveAndExportDocs(_.flatten(objs));
};
function retrieveAndExportDocs(objs) {
es.mget({
index: config.file.kibana_index,
body: {docs: objs.map(transformToMget)}
})
.then(function (response) {
saveToFile(response.docs.map(_.partialRight(_.pick, '_id', '_type', '_source')));
});
}
// Takes an object and returns the associated data needed for an mget API request
function transformToMget(obj) {
return {_id: obj.id, _type: obj.type};
}
function saveToFile(results) {
var blob = new Blob([angular.toJson(results, true)], {type: 'application/json'});
saveAs(blob, 'export.json');
}
$scope.importAll = function (fileContents) {
var docs;
try {
docs = JSON.parse(fileContents);
} catch (e) {
notify.error('The file could not be processed.');
}
return es.mget({
index: config.file.kibana_index,
body: {docs: docs.map(_.partialRight(_.pick, '_id', '_type'))}
})
.then(function (response) {
var existingDocs = _.where(response.docs, {found: true});
var confirmMessage = 'The following objects will be overwritten:\n\n';
if (existingDocs.length === 0 || window.confirm(confirmMessage + _.pluck(existingDocs, '_id').join('\n'))) {
return es.bulk({
index: config.file.kibana_index,
body: _.flatten(docs.map(transformToBulk))
})
.then(refreshIndex)
.then(refreshData, notify.error);
}
});
};
$scope.changeTab = function (obj) {
$state.tab = obj.title;
// Takes a doc and returns the associated two entries for an index bulk API request
function transformToBulk(doc) {
return [
{index: _.pick(doc, '_id', '_type')},
doc._source
];
}
function refreshIndex() {
return es.indices.refresh({
index: config.file.kibana_index
});
}
function refreshData() {
return getData($scope.advancedFilter);
}
$scope.changeTab = function (tab) {
$scope.currentTab = tab;
$scope.selectedItems.length = 0;
$state.tab = tab.title;
$state.save();
resetCheckBoxes();
};
$scope.$watch('advancedFilter', function (filter) {
getData(filter);
});
}
};
});

View file

@ -24,6 +24,7 @@
<textarea rows="1" msd-elastic ng-if="field.type === 'text'" ng-model="field.value" class="form-control span12"/>
<input ng-if="field.type === 'number'" type="number" ng-model="field.value" class="form-control span12"/>
<div ng-if="field.type === 'json' || field.type === 'array'" ui-ace="{ onLoad: aceLoaded, mode: 'json' }" id="{{field.name}}" ng-model="field.value" class="form-control"></div>
<input ng-if="field.type === 'boolean'" type="checkbox" ng-model="field.value" ng-checked="field.value">
</div>
</form>
<div class="form-group">

View file

@ -29,7 +29,7 @@ define(function (require) {
*
* @param {array} memo The stack of fields
* @param {mixed} value The value of the field
* @param {stirng} key The key of the field
* @param {string} key The key of the field
* @param {object} collection This is a reference the collection being reduced
* @param {array} parents The parent keys to the field
* @returns {array}
@ -55,11 +55,12 @@ define(function (require) {
} else if (_.isArray(field.value)) {
field.type = 'array';
field.value = angular.toJson(field.value, true);
} else if (_.isBoolean(field.value)) {
field.type = 'boolean';
field.value = field.value;
} else if (_.isPlainObject(field.value)) {
// do something recursive
return _.reduce(field.value, _.partialRight(createField, parents), memo);
} else {
return;
}
memo.push(field);
@ -74,15 +75,10 @@ define(function (require) {
$scope.title = inflection.singularize(serviceObj.title);
es.get({
index: config.file.kibana_index,
type: service.type,
id: $routeParams.id
})
.then(function (obj) {
service.get($routeParams.id).then(function (obj) {
$scope.obj = obj;
$scope.link = service.urlFor(obj._id);
$scope.fields = _.reduce(obj._source, createField, []);
$scope.link = service.urlFor(obj.id);
$scope.fields = _.reduce(_.defaults(obj.serialize(), obj.defaults), createField, []);
})
.catch(notify.fatal);

View file

@ -48,12 +48,18 @@ kbn-settings-objects {
font-weight: normal;
}
.delete-all {
.btn {
font-size: 10px;
margin-left: 20px;
}
}
.header {
.title, .controls {
padding-right: 1em;
display: inline-block;
}
}
}
kbn-settings-advanced {

View file

@ -17,4 +17,10 @@
Scale Y-Axis to Data Bounds
</label>
</div>
<div class="vis-option-item" ng-show="vis.hasSchemaAgg('segment', 'date_histogram')">
<label>
<input type="checkbox" ng-model="vis.params.addTimeMarker" ng-checked="vis.params.addTimeMarker">
Current time marker
</label>
</div>
</div>

View file

@ -7,13 +7,14 @@ define(function (require) {
_(VislibRenderbot).inherits(Renderbot);
function VislibRenderbot(vis, $el) {
VislibRenderbot.Super.call(this, vis, $el);
this.vislibVis = {};
this._createVis();
}
VislibRenderbot.prototype._createVis = function () {
var self = this;
if (self.vislibVis) self.destroy();
self.vislibParams = self._getVislibParams();
self.vislibVis = new vislib.Vis(self.$el[0], self.vislibParams);

View file

@ -20,7 +20,9 @@ define(function (require) {
scale: 'linear',
mode: 'stacked',
interpolate: 'linear',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],

View file

@ -16,7 +16,9 @@ define(function (require) {
addLegend: true,
scale: 'linear',
mode: 'stacked',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'percentage', 'grouped'],

View file

@ -20,7 +20,9 @@ define(function (require) {
drawLinesBetweenPoints: true,
radiusRatio: 9,
scale: 'linear',
defaultYExtents: false
defaultYExtents: false,
times: [],
addTimeMarker: false
},
scales: ['linear', 'log', 'square root'],
editor: require('text!plugins/vis_types/vislib/editors/line.html')

View file

@ -15,7 +15,7 @@ define(function (require) {
mapType: 'Scaled Circle Markers',
isDesaturated: true
},
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Pins'],
mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid'],
editor: require('text!plugins/vis_types/vislib/editors/tile_map.html')
},
responseConverter: geoJsonConverter,

View file

@ -1,84 +1,81 @@
<ng-form name="aggForm">
<!-- header -->
<div class="vis-editor-agg-header">
<!-- header -->
<div class="vis-editor-agg-header">
<!-- open/close editor -->
<button
aria-label="{{ editorOpen ? 'Close Editor' : 'Open Editor' }}"
ng-click="editorOpen = !editorOpen"
type="button"
class="btn btn-xs vis-editor-agg-header-toggle">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
</button>
<!-- open/close editor -->
<!-- title -->
<span class="vis-editor-agg-header-title">
{{ agg.schema.title }}
</span>
<!-- description -->
<span ng-if="!editorOpen && aggForm.$valid" class="vis-editor-agg-header-description">
{{ describe() }}
</span>
<!-- error -->
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
{{ aggForm.describeErrors() }}
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<button
aria-label="{{ editorOpen ? 'Close Editor' : 'Open Editor' }}"
ng-click="editorOpen = !editorOpen"
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs vis-editor-agg-header-toggle">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-up"></i>
</button>
<!-- title -->
<span class="vis-editor-agg-header-title">
{{ agg.schema.title }}
</span>
<!-- down button -->
<button
aria-lebl="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-down"></i>
</button>
<!-- description -->
<span ng-if="!editorOpen && aggForm.$valid" class="vis-editor-agg-header-description">
{{ describe() }}
</span>
<!-- error -->
<span ng-if="!editorOpen && aggForm.$invalid" class="vis-editor-agg-header-description danger">
{{ aggForm.describeErrors() }}
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<button
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-up"></i>
</button>
<!-- down button -->
<button
aria-lebl="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-default">
<i aria-hidden="true" class="fa fa-caret-down"></i>
</button>
<!-- remove button -->
<button
ng-if="canRemove(agg)"
aria-label="Remove Dimension"
ng-if="stats.count > stats.min"
ng-click="remove(agg)"
tooltip="Remove Dimension"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-danger">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
<!-- remove button -->
<button
ng-if="canRemove(agg)"
aria-label="Remove Dimension"
ng-if="stats.count > stats.min"
ng-click="remove(agg)"
tooltip="Remove Dimension"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-danger">
<i aria-hidden="true" class="fa fa-times"></i>
</button>
</div>
</div>
<vis-editor-agg-params
agg="agg"
group-name="groupName"
ng-show="editorOpen"
class="vis-editor-agg-editor">
</vis-editor-agg-params>
<vis-editor-agg-params
agg="agg"
group-name="groupName"
ng-show="editorOpen"
class="vis-editor-agg-editor">
</vis-editor-agg-params>
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>
</ng-form>
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>

View file

@ -15,10 +15,14 @@ define(function (require) {
});
return {
restrict: 'E',
restrict: 'A',
template: require('text!plugins/visualize/editor/agg.html'),
link: function ($scope, $el) {
$scope.editorOpen = $scope.agg.brandNew;
require: 'form',
link: function ($scope, $el, attrs, kbnForm) {
$scope.editorOpen = !!$scope.agg.brandNew;
if (!$scope.editorOpen) {
$scope.$evalAsync(kbnForm.$setTouched);
}
$scope.$watchMulti([
'$index',

View file

@ -11,20 +11,21 @@
</ul>
</div>
<button
<div
ng-if="stats.max > stats.count"
type="button"
ng-init="add.form = stats.count < 1 ? !add.form : add.form"
ng-click="add.form = !add.form"
class="vis-editor-agg-wide-btn btn btn-xs btn-default" >
class="vis-editor-agg-wide-btn">
<div ng-if="!add.form">
<span ng-if="groupName !== 'buckets' || !stats.count">
<i aria-hidden="true" class="fa fa-plus"></i> Add Aggregation
</span>
<span ng-if="groupName === 'buckets' && stats.count > 0">
<i aria-hidden="true" class="fa fa-code-fork"></i> Add Sub Aggregation
</span>
<div class="vis-editor-agg-wide-btn-add" ng-if="groupName !== 'buckets' || !stats.count">
<i aria-hidden="true" class="fa fa-plus"></i> Add {{ groupName }}
</div>
<div class="vis-editor-agg-wide-btn-add" ng-if="groupName === 'buckets' && stats.count > 0">
<i aria-hidden="true" class="fa fa-code-fork"></i> Add sub-{{ groupName }}
</div>
</div>
<span ng-if="add.form">cancel</span>
</button>
<div class="vis-editor-agg-wide-btn-add" ng-if="add.form">
Cancel
</div>
</div>

View file

@ -14,9 +14,9 @@
</nesting-indicator>
<!-- agg.html - controls for aggregation -->
<vis-editor-agg class="vis-editor-agg"></vis-editor-agg>
<ng-form vis-editor-agg name="aggForm" class="vis-editor-agg"></ng-form>
</div>
<vis-editor-agg-add ng-if="stats.count === 0" class="vis-editor-agg-add"></vis-editor-agg-add>
</div>
</li>
</li>

View file

@ -8,6 +8,7 @@ define(function (require) {
require('directives/saved_object_finder');
require('components/visualize/visualize');
require('components/clipboard/clipboard');
require('components/comma_list_filter');
require('filters/uriescape');

View file

@ -7,42 +7,68 @@
{{ indexPattern.id }}
</div>
<ul class="list-unstyled">
<!-- metrics -->
<vis-editor-agg-group ng-if="vis.type.schemas.metrics" group-name="metrics"></vis-editor-agg-group>
<nav class="navbar navbar-default navbar-static-top subnav">
<div class="container-fluid">
<!-- buckets -->
<vis-editor-agg-group ng-if="vis.type.schemas.buckets" group-name="buckets"></vis-editor-agg-group>
<!-- tabs -->
<ul class="nav navbar-nav">
<li ng-class="{active: sidebar.section == 'data'}" ng-show="vis.type.schemas.metrics">
<a class="navbar-link active" ng-click="sidebar.section='data'">Data</a>
</li>
<li ng-class="{active: sidebar.section == 'options'}">
<a class="navbar-link" ng-click="sidebar.section='options'">Options</a>
</li>
</ul>
<!-- vis options -->
<vis-editor-vis-options
vis="vis"
>
</vis-editor-vis-options>
<!-- controls -->
<ul class="nav navbar-nav navbar-right">
<li ng-if="visualizeEditor.softErrorCount() > 0"
disabled
tooltip="{{ visualizeEditor.describeErrors() }}" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<a class="danger navbar-link">
<i class="fa fa-warning"></i>
</a>
</li>
<li tooltip="Apply changes" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<button class="btn-success navbar-btn-link"
type="submit"
ng-disabled="!vis.dirty">
<!-- apply/discard -->
</ul>
<div class="vis-editor-sidebar-buttons">
<p
ng-if="visualizeEditor.softErrorCount() > 0"
class="text-center text-danger sidebar-item-text">
<i aria-hidden="true" class="fa fa-warning"></i>
{{ visualizeEditor.describeErrors() }}
</p>
<button
ng-disabled="!vis.dirty"
type="submit"
class="sidebar-item-button success">
Apply
</button>
<button
ng-disabled="!vis.dirty"
type="button"
ng-click="resetEditableVis()"
class="sidebar-item-button default">
Discard
</button>
<i class="fa fa-play"></i>
</button>
</li>
<li tooltip="Discard changes" tooltip-placement="bottom" tooltip-popup-delay="400" tooltip-append-to-body="1">
<button class="btn-default navbar-btn-link"
ng-disabled="!vis.dirty"
ng-click="resetEditableVis()">
<i class="fa fa-close"></i>
</button>
</li>
</ul>
</div>
</nav>
<div class="vis-editor-config" ng-show="sidebar.section == 'data'">
<ul class="list-unstyled">
<!-- metrics -->
<vis-editor-agg-group ng-if="vis.type.schemas.metrics" group-name="metrics"></vis-editor-agg-group>
<!-- buckets -->
<vis-editor-agg-group ng-if="vis.type.schemas.buckets" group-name="buckets"></vis-editor-agg-group>
</ul>
</div>
<div class="vis-editor-config" ng-show="sidebar.section == 'options'">
<ul class="list-unstyled">
<!-- vis options -->
<vis-editor-vis-options vis="vis"></vis-editor-vis-options>
</ul>
</div>
</form>
</div>

View file

@ -11,9 +11,11 @@ define(function (require) {
restrict: 'E',
template: require('text!plugins/visualize/editor/sidebar.html'),
scope: true,
link: function ($scope) {
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$bind('vis', 'editableVis');
this.section = _.get($scope, 'vis.type.requiresSearch') ? 'data' : 'options';
}
};
});
});
});

View file

@ -6,6 +6,23 @@
@vis-editor-nesting-width: 8px;
@vis-editor-agg-editor-spacing: 5px;
// For the vis-editor sidebar nav
.navbar-default .navbar-nav {
&> .active > a:before {
border: 7px solid @gray-lighter;
border-color: transparent transparent @gray-lighter transparent;
}
.danger {
color: @btn-danger-color;
background-color: @btn-danger-bg;
}
}
.btn-xs {
line-height: 1.3;
}
navbar {
.bitty-modal-container {
position: relative;
@ -73,7 +90,7 @@
form {
.flex-parent(1, 1, auto);
> ul {
> div.vis-editor-config {
.flex(1, 1, auto);
overflow-y: auto;
}
@ -81,16 +98,25 @@
> .vis-edit-sidebar-buttons {
.flex(0, 0, auto)
}
label {
margin-bottom: 0px;
}
}
}
.sidebar-item-title {
background: @sidebar-bg;
font-size: 20px;
font-weight: bold;
border: inherit !important;
background-color: @gray-lighter;
margin-bottom: 5px;
padding: 2px 5px !important;
}
.sidebar-item-title:hover {
color: @sidebar-header-color !important;
background-color: @sidebar-bg !important;
color: @text-color !important;
background-color: @gray-lighter !important;
}
}
@ -119,7 +145,6 @@
.flex-parent();
padding: @vis-editor-agg-editor-spacing;
border-bottom: 1px solid @sidebar-bg;
// wraps the .vis-editor-agg and nesting-indicator ^^
&-wrapper {
@ -226,6 +251,24 @@
&-wide-btn {
.border-radius(0);
border-top: 2px solid @gray-lighter;
&-add {
width: 140px;
margin: -2px auto 5px auto;
text-align: center;
border: 2px solid @gray-lighter;
border-top: 0px;
padding: 3px;
border-bottom-right-radius: @border-radius-base;
border-bottom-left-radius: @border-radius-base;
background-color: @body-bg;
font-weight: bold;
}
&-add:hover {
background-color: @gray-lighter;
}
}
&-add {

View file

@ -1,13 +1,6 @@
<li class="sidebar-item" ng-show="vis.type.params.editor">
<div ng-hide="alwaysShowOptions" class="sidebar-item-title" ng-click="showVisOptions = !showVisOptions">
<div class="sidebar-item-title">
view options
<i
aria-hidden="true"
class="fa fa-caret-down"
ng-class="{'fa-caret-down': showVisOptions, 'fa-caret-right': !showVisOptions}">
</i>
</div>
<div ng-show="alwaysShowOptions" class="sidebar-item-title">view options</div>
<div class="visualization-options" ng-show="alwaysShowOptions || showVisOptions"></div>
<div class="visualization-options"></div>
</li>

View file

@ -15,19 +15,19 @@ kbn-table, .kbn-table, tbody[kbn-rows] {
dl.source {
margin-bottom: 0;
line-height: 2em;
word-break: break-all;
dt, dd {
display: inline;
}
dt {
display: inline;
background: @gray-lighter;
padding: @padding-xs-vertical @padding-xs-horizontal;
margin-right: @padding-xs-horizontal;
font-family: monospace;
}
dd {
display: inline;
word-break: break-all;
word-break: normal;
}
}
}
}

View file

@ -468,3 +468,6 @@ style-compile {
background-color: @gray-lighter;
}
mark, .mark {
background-color: rgba(252, 229, 113, 1);
}

View file

@ -13,6 +13,17 @@
-moz-box-shadow: inset 0 -10px 10px -12px #333;
-webkit-box-shadow: inset 0 -10px 10px -12px #333;
&-btn-link {
height: @navbar-height;
margin: 0;
border-radius: 0;
@media (max-width: @screen-sm-min) {
width: 100%;
text-align: left;
}
}
&-default {
.badge {

View file

@ -256,5 +256,25 @@ define(function (require) {
get: function (obj, path) {
return _.deepGet(obj, path);
},
/**
* Parse a comma-seperated list into an array
* efficiently, or just return if already an array
*
* @param {string|array} input - the comma-seperated list
* @return {array}
*/
commaSeperatedList: function (input) {
if (_.isArray(input)) return input;
var source = String(input || '').split(',');
var list = [];
while (source.length) {
var item = source.shift().trim();
if (item) list.push(item);
}
return list;
},
};
});

View file

@ -3,29 +3,36 @@ define(function () {
var lineSize = 0;
var newText = '';
var inHtmlTag = false;
var inHtmlChar = false;
for (var i = 0, len = text.length; i < len; i++) {
var chr = text.charAt(i);
newText += chr;
switch (chr) {
case ' ':
case ';':
case ':':
case ',':
// natural line break, reset line size
lineSize = 0;
break;
case '<':
inHtmlTag = true;
break;
case '>':
inHtmlTag = false;
lineSize = 0;
break;
default:
if (!inHtmlTag) lineSize++;
break;
case ' ':
case ':':
case ',':
// natural line break, reset line size
lineSize = 0;
break;
case '<':
inHtmlTag = true;
break;
case '>':
inHtmlTag = false;
lineSize = 0;
break;
case '&':
inHtmlChar = true;
break;
case ';':
inHtmlChar = false;
lineSize = 0;
break;
default:
if (!inHtmlTag && !inHtmlChar) lineSize++;
break;
}
if (lineSize > minLineLength) {
@ -38,4 +45,4 @@ define(function () {
return newText;
};
});
});

68
src/kibana/utils/range.js Normal file
View file

@ -0,0 +1,68 @@
define(function (require) {
var _ = require('lodash');
/**
* Regexp portion that matches our number
*
* supports:
* -100
* -100.0
* 0
* 0.10
* Infinity
* -Infinity
*
* @type {String}
*/
var _RE_NUMBER = '(\\-?(?:\\d+(?:\\.\\d+)?|Infinity))';
/**
* Regexp for the interval notation
*
* supports:
* [num, num]
* ( num , num ]
* [Infinity,num)
*
* @type {RegExp}
*/
var RANGE_RE = new RegExp('^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$');
function parse(input) {
var match = String(input).match(RANGE_RE);
if (!match) {
throw new TypeError('expected input to be in interval notation eg. (100, 200]');
}
return new Range(
match[1] === '[',
parseFloat(match[2]),
parseFloat(match[3]),
match[4] === ']'
);
}
function Range(/* minIncl, min, max, maxIncl */) {
var args = _.toArray(arguments);
if (args[1] > args[2]) args.reverse();
this.minInclusive = args[0];
this.min = args[1];
this.max = args[2];
this.maxInclusive = args[3];
}
Range.prototype.within = function (n) {
if (this.min === n && !this.minInclusive) return false;
if (this.min > n) return false;
if (this.max === n && !this.maxInclusive) return false;
if (this.max < n) return false;
return true;
};
return parse;
});

View file

@ -10,7 +10,7 @@ define(function (require) {
'=' : '-equal-'
};
_.each(trans, function (val, key) {
var regex = new RegExp(key);
var regex = new RegExp(key, 'g');
id = id.replace(regex, val);
});
id = id.replace(/[\s]+/g, '-');

View file

@ -16,6 +16,17 @@ function checkPath(path) {
}
}
// Set defaults for config file stuff
kibana.port = kibana.port || 5601;
kibana.host = kibana.host || '0.0.0.0';
kibana.elasticsearch_url = kibana.elasticsearch_url || 'http://localhost:9200';
kibana.maxSockets = kibana.maxSockets || Infinity;
kibana.log_file = kibana.log_file || null;
kibana.request_timeout = kibana.startup_timeout == null ? 0 : kibana.request_timeout;
kibana.ping_timeout = kibana.ping_timeout == null ? kibana.request_timeout : kibana.ping_timeout;
kibana.startup_timeout = kibana.startup_timeout == null ? 5000 : kibana.startup_timeout;
// Check if the local public folder is present. This means we are running in
// the NPM module. If it's not there then we are running in the git root.
var public_folder = path.resolve(__dirname, '..', 'public');
@ -34,9 +45,9 @@ try {
}
var config = module.exports = {
port : kibana.port || 5601,
host : kibana.host || '0.0.0.0',
elasticsearch : kibana.elasticsearch_url || 'http://localhost:9200',
port : kibana.port,
host : kibana.host,
elasticsearch : kibana.elasticsearch_url,
root : path.normalize(path.join(__dirname, '..')),
quiet : false,
public_folder : public_folder,
@ -46,8 +57,10 @@ var config = module.exports = {
package : require(packagePath),
htpasswd : htpasswdPath,
buildNum : '@@buildNum',
maxSockets : kibana.maxSockets || Infinity,
log_file : kibana.log_file || null
maxSockets : kibana.maxSockets,
log_file : kibana.log_file,
request_timeout : kibana.request_timeout,
ping_timeout : kibana.ping_timeout
};
config.plugins = listPlugins(config);

View file

@ -33,6 +33,10 @@ kibana_index: ".kibana"
# The default application to load.
default_app_id: "discover"
# Time in milliseconds to wait for elasticsearch to respond to pings, defaults to
# request_timeout setting
# ping_timeout: 1500
# Time in milliseconds to wait for responses from the back end or elasticsearch.
# This must be > 0
request_timeout: 300000
@ -41,6 +45,9 @@ request_timeout: 300000
# Set to 0 to disable.
shard_timeout: 0
# Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying
# startup_timeout: 5000
# Set to false to have a complete disregard for the validity of the SSL
# certificate.
verify_ssl: true

View file

@ -24,7 +24,7 @@ if (config.kibana.ca) {
module.exports = new elasticsearch.Client({
host: url.format(uri),
ssl: ssl,
pingTimeout: config.kibana.ping_timeout,
pingTimeout: config.ping_timeout,
log: function (config) {
this.error = function (err) {
logger.error({ err: err });

View file

@ -6,7 +6,7 @@ var logger = require('./logger');
var config = require('../config');
function waitForPong() {
return client.ping()
return client.ping({requestTimeout: config.kibana.startup_timeout})
.catch(function (err) {
if (!(err instanceof NoConnections)) throw err;

View file

@ -75,7 +75,7 @@ router.use(function (req, res, next) {
method: req.method,
headers: _.defaults({}, req.headers),
strictSSL: config.kibana.verify_ssl,
timeout: config.kibana.request_timeout
timeout: config.request_timeout
};
options.headers['x-forward-for'] = req.connection.remoteAddress || req.socket.remoteAddress;

View file

@ -1,12 +1,5 @@
module.exports = function (grunt) {
var config = {
test: {
files: [
'<%= unitTestDir %>/**/*.js'
],
tasks: ['mocha:unit']
},
less: {
files: [
'<%= app %>/**/styles/**/*.less',

View file

@ -36,6 +36,11 @@
ScreencastReporter: '../../node_modules/mocha-screencast-reporter/screencast-reporter'
},
shim: {
angular_src: {
deps: [
'jquery'
]
},
angular: {
deps: [
'jquery',

View file

@ -1,31 +1,33 @@
define(function (require) {
describe('PercentList directive', function () {
describe('NumberList directive', function () {
var $ = require('jquery');
var _ = require('lodash');
var simulateKeys = require('test_utils/simulate_keys');
require('components/agg_types/controls/_values_list');
require('components/number_list/number_list');
var $el;
var $scope;
var compile;
function onlyValidValues() {
return $el.find('[ng-model]').toArray().map(function (el) {
var ngModel = $(el).controller('ngModel');
return ngModel.$valid ? ngModel.$modelValue : undefined;
});
}
beforeEach(module('kibana'));
beforeEach(inject(function ($injector) {
var $compile = $injector.get('$compile');
var $rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
$el = $('<div>').append(
$('<input>')
.attr('ng-model', 'vals[$index]')
.attr('ng-repeat', 'val in vals')
.attr('values-list', 'vals')
.attr('values-list-min', '0')
.attr('values-list-max', '100')
);
compile = function (vals) {
$el = $('<kbn-number-list ng-model="vals">');
compile = function (vals, range) {
$scope.vals = vals || [];
$el.attr('range', range);
$compile($el)($scope);
$scope.$apply();
};
@ -38,51 +40,22 @@ define(function (require) {
it('fails on invalid numbers', function () {
compile([1, 'foo']);
expect($scope.vals).to.eql([1, undefined]);
expect($el.find('.ng-invalid').size()).to.be(1);
expect(onlyValidValues()).to.eql([1, undefined]);
});
it('supports decimals', function () {
compile(['1.2', '000001.6', '99.10']);
expect($scope.vals).to.eql([1.2, 1.6, 99.1]);
expect(onlyValidValues()).to.eql([1.2, 1.6, 99.1]);
});
it('ensures that the values are in order', function () {
compile([1, 2, 3, 10, 4, 5]);
expect($scope.vals).to.eql([1, 2, 3, undefined, 4, 5]);
expect($el.find('.ng-invalid').size()).to.be(1);
expect(onlyValidValues()).to.eql([1, 2, 3, 10, undefined, 5]);
});
describe('ensures that the values are between 0 and 100', function () {
it(': -1', function () {
compile([-1, 1]);
expect($scope.vals).to.eql([undefined, 1]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it(': 0', function () {
compile([0, 1]);
expect($scope.vals).to.eql([undefined, 1]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it(': 0.0001', function () {
compile([0.0001, 1]);
expect($scope.vals).to.eql([0.0001, 1]);
expect($el.find('.ng-invalid').size()).to.be(0);
});
it(': 99.9999999', function () {
compile([1, 99.9999999]);
expect($scope.vals).to.eql([1, 99.9999999]);
expect($el.find('.ng-invalid').size()).to.be(0);
});
it(': 101', function () {
compile([1, 101]);
expect($scope.vals).to.eql([1, undefined]);
expect($el.find('.ng-invalid').size()).to.be(1);
});
it('requires that values are within a range', function () {
compile([50, 100, 200, 250], '[100, 200)');
expect(onlyValidValues()).to.eql([undefined, 100, undefined, undefined]);
});
describe('listens for keyboard events', function () {
@ -94,7 +67,7 @@ define(function (require) {
['up', 'up', 'up']
)
.then(function () {
expect($scope.vals).to.eql([4]);
expect(onlyValidValues()).to.eql([4]);
});
});
@ -118,7 +91,7 @@ define(function (require) {
seq
)
.then(function () {
expect($scope.vals).to.eql([5.1]);
expect(onlyValidValues()).to.eql([5.1]);
});
});
@ -130,7 +103,7 @@ define(function (require) {
['down', 'down', 'down']
)
.then(function () {
expect($scope.vals).to.eql([2]);
expect(onlyValidValues()).to.eql([2]);
});
});
@ -154,7 +127,7 @@ define(function (require) {
seq
)
.then(function () {
expect($scope.vals).to.eql([4.8]);
expect(onlyValidValues()).to.eql([4.8]);
});
});
@ -170,9 +143,9 @@ define(function (require) {
return simulateKeys(getEl, seq)
.then(function () {
expect($scope.vals).to.eql([9, 10, 13]);
expect(onlyValidValues()).to.eql([9, 10, 13]);
});
});
});
});
});
});

View file

@ -0,0 +1,32 @@
define(function (require) {
require('components/comma_list_filter');
describe('Comma-List filter', function () {
var commaList;
beforeEach(module('kibana'));
beforeEach(inject(function ($injector) {
commaList = $injector.get('commaListFilter');
}));
it('converts a string to a pretty list', function () {
expect(commaList('john,jaine,jim', true)).to.be('john, jaine and jim');
expect(commaList('john,jaine,jim', false)).to.be('john, jaine or jim');
});
it('can accept an array too', function () {
expect(commaList(['john', 'jaine', 'jim'])).to.be('john, jaine or jim');
});
it('handles undefined ok', function () {
expect(commaList()).to.be('');
});
it('handls single values ok', function () {
expect(commaList(['john'])).to.be('john');
});
});
});

View file

@ -0,0 +1,74 @@
define(function (require) {
describe('config component', function () {
var $scope;
var config;
var defaults;
var configFile;
beforeEach(module('kibana'));
beforeEach(inject(function ($injector, Private) {
config = $injector.get('config');
$scope = $injector.get('$rootScope');
configFile = $injector.get('configFile');
defaults = Private(require('components/config/defaults'));
}));
it('exposes the configFile', function () {
expect(config.file).to.be(configFile);
});
describe('#get', function () {
it('gives access to config values', function () {
expect(config.get('dateFormat')).to.be.a('string');
});
it('reads from the defaults', function () {
var initial = config.get('dateFormat');
var newDefault = initial + '- new';
defaults.dateFormat.value = newDefault;
expect(config.get('dateFormat')).to.be(newDefault);
});
});
describe('#set', function () {
it('stores a value in the config val set', function () {
var initial = config.get('dateFormat');
config.set('dateFormat', 'notaformat');
expect(config.get('dateFormat')).to.be('notaformat');
});
});
describe('#$bind', function () {
it('binds a config key to a $scope property', function () {
var dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
});
it('alows overriding the property name', function () {
var dateFormat = config.get('dateFormat');
config.$bind($scope, 'dateFormat', 'defaultDateFormat');
expect($scope).to.not.have.property('dateFormat');
expect($scope).to.have.property('defaultDateFormat', dateFormat);
});
it('keeps the property up to date', function () {
var dateFormat = config.get('dateFormat');
var newDateFormat = dateFormat + ' NEW NEW NEW!';
config.$bind($scope, 'dateFormat');
expect($scope).to.have.property('dateFormat', dateFormat);
config.set('dateFormat', newDateFormat);
expect($scope).to.have.property('dateFormat', newDateFormat);
});
});
});
});

View file

@ -23,6 +23,9 @@ define(function (require) {
'team': { type: 'nested' },
'team.name': { type: 'string' },
'team.role': { type: 'string' },
'user': { type: 'conflict' },
'user.name': { type: 'string' },
'user.id': { type: 'conflict' },
'delta': { type: 'number', scripted: true }
}
}
@ -45,7 +48,8 @@ define(function (require) {
{ name: 'foo', role: 'leader' },
{ name: 'bar', role: 'follower' },
{ name: 'baz', role: 'party boy' },
]
],
user: { name: 'smith', id: 123 }
},
fields: {
delta: [42],
@ -73,6 +77,12 @@ define(function (require) {
expect(flat.groups).to.eql(['loners']);
});
it('flattens conflicting types in the mapping', function () {
expect(flat).to.not.have.property('user');
expect(flat).to.have.property('user.name', hit._source.user.name);
expect(flat).to.have.property('user.id', hit._source.user.id);
});
it('preserves objects in arrays', function () {
expect(flat).to.have.property('tags', hit._source.tags);
});

View file

@ -64,7 +64,7 @@ define(function (require) {
// make the element
$elem = angular.element(
'<vis-editor-agg></vis-editor-agg>'
'<ng-form vis-editor-agg></ng-form>'
);
// compile the html
@ -77,7 +77,7 @@ define(function (require) {
$scope = $elem.isolateScope();
}));
it('should only add the close button only if there is more than the minimum', function () {
it('should only add the close button if there is more than the minimum', function () {
expect($parentScope.canRemove($parentScope.agg)).to.be(false);
$parentScope.group.push({
id: '3',

View file

@ -8,11 +8,11 @@ define(function (require) {
['aaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaa<wbr>aaaaaaaaa'],
['aaaa aaaaaaaaaaaaaaa', 'aaaa aaaaaaaaaaa<wbr>aaaa'],
['aaaa;aaaaaaaaaaaaaaa', 'aaaa;aaaaaaaaaaa<wbr>aaaa'],
['aaaa&aaaaaaaaaaaaaaa', 'aaaa&aaaaaa<wbr>aaaaaaaaa'],
['aaaa:aaaaaaaaaaaaaaa', 'aaaa:aaaaaaaaaaa<wbr>aaaa'],
['aaaa,aaaaaaaaaaaaaaa', 'aaaa,aaaaaaaaaaa<wbr>aaaa'],
['aaaa aaaa', 'aaaa aaaa'],
['aaaa <mark>aaaa</mark>aaaaaaaaaaaa', 'aaaa <mark>aaaa</mark>aaaaaaaaaaa<wbr>a']
['aaaa <mark>aaaa</mark>aaaaaaaaaaaa', 'aaaa <mark>aaaa</mark>aaaaaaaaaaa<wbr>a'],
['aaaa&quot;aaaaaaaaaaaa', 'aaaa&quot;aaaaaaaaaaa<wbr>a']
];
_.each(fixtures, function (fixture) {

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