resolving conflict with master

This commit is contained in:
Juan Thomassie 2014-08-22 15:42:31 -05:00
commit c1fe26e314
154 changed files with 6282 additions and 2838 deletions

View file

@ -4,30 +4,22 @@
- **src/kibana/apps/dashboard/directives/grid.js**
- change this from event based to calling a method on dashboardApp (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/dashboard/directives/grid.js)
- **src/kibana/apps/discover/controllers/discover.js**
- Switch this to watching time.string when we implement it (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
- On array fields, negating does not negate the combination, rather all terms (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
- Move to utility class (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
- Move to utility class (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
- a legit way to update the index pattern (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/discover/controllers/discover.js)
- **src/kibana/apps/settings/sections/indices/_create.js**
- we should probably display a message of some kind (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/settings/sections/indices/_create.js)
- **src/kibana/apps/visualize/controllers/editor.js**
- Switch this to watching time.string when we implement it (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/controllers/editor.js)
- **src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js**
- Should we abtract out the agg building stuff? (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js)
- Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_adhoc_vis.js)
- **src/kibana/apps/visualize/saved_visualizations/_type_defs.js**
- We need to be able to get ahold of angular services here (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js)
- We need to be able to get ahold of angular services here (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/_type_defs.js)
- **src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js**
- We need more just _count here. (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/terms.js)
- **src/kibana/components/agg_types/buckets/terms.js**
- We need more than just _count here. (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/agg_types/buckets/terms.js)
- **src/kibana/components/index_patterns/_mapper.js**
- Change index to be the resolved in some way, last three months, last hour, last year, whatever (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_mapper.js)
- **src/kibana/components/state_management/state.js**
- Change all the references to onUpdate to the actual fetch_with_changes event (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/state_management/state.js)
- **src/kibana/components/visualize/visualize.js**
- we need to have some way to clean up result requests (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/visualize/visualize.js)
- **src/kibana/directives/rows.js**
- It would be better to actually check the type of the field, but we don't have (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/directives/rows.js)
- **src/kibana/services/timefilter.js**
- This should be disabled on route change, apps need to enable it explicitly (https://github.com/elasticsearch/kibana4/blob/master/src/kibana/services/timefilter.js)
- **test/unit/specs/apps/dashboard/directives/panel.js**
- This should not be needed, timefilter is only included here (https://github.com/elasticsearch/kibana4/blob/master/test/unit/specs/apps/dashboard/directives/panel.js)
- **test/unit/specs/directives/timepicker.js**

View file

@ -46,7 +46,7 @@
<div class="spinner">
</div>
</li>
<li ng-if="setupComplete" ng-show="opts.timefilter.enabled()" class="navbar-timepicker-display">
<li ng-if="setupComplete" ng-show="opts.timefilter.enabled" class="navbar-timepicker-display">
<a ng-click="toggleTimepicker()">
<pretty-duration from="opts.timefilter.time.from" to="opts.timefilter.time.to"></pretty-duration>
&nbsp;

View file

@ -1,11 +1,13 @@
define(function (require) {
var app = require('modules').get('app/dashboard');
var _ = require('lodash');
require('modules')
.get('app/dashboard')
.directive('dashboardPanel', function (savedVisualizations, Notifier) {
var _ = require('lodash');
require('apps/visualize/directives/visualize');
app.directive('dashboardPanel', function (savedVisualizations, Notifier) {
var notify = new Notifier();
require('components/visualize/visualize');
return {
restrict: 'E',
template: require('text!apps/dashboard/partials/panel.html'),
@ -20,14 +22,12 @@ define(function (require) {
if (!$scope.panel.visId) return;
savedVisualizations.get($scope.panel.visId)
.then(function (vis) {
$scope.vis = vis;
.then(function (savedVis) {
$scope.savedVis = savedVis;
// .destroy() called by the visualize directive
})
.catch(function (e) {
$scope.vis = {
error: e
};
$scope.error = e.message;
console.log(e);
});
});

View file

@ -73,7 +73,7 @@ define(function (require) {
$scope.openAdd = _.partial($scope.configTemplate.toggle, 'pickVis');
$scope.refresh = _.bindKey(courier, 'fetch');
timefilter.enabled(true);
timefilter.enabled = true;
$scope.timefilter = timefilter;
$scope.$watchCollection('globalState.time', $scope.refresh);

View file

@ -1,11 +1,16 @@
<div class="panel panel-default">
<div class="panel-heading">
<span class="panel-title">{{vis.title}}</span>
<span class="panel-title">{{savedVis.title}}</span>
<div class="btn-group">
<a ng-show="!appEmbedded" ng-href="#visualize/edit/{{vis.title | uriescape}}"><i class="fa fa-pencil"></i></a>
<a ng-show="!appEmbedded" ng-href="#visualize/edit/{{savedVis.title | uriescape}}"><i class="fa fa-pencil"></i></a>
<a ng-show="!appEmbedded" ng-click="remove()"><i class="fa fa-times"></i></a>
</div>
<div class="clearfix"></div>
</div>
<visualize vis="vis"></visualize>
<div ng-if="error" class="load-error">
<i class="fa fa-exclamation-triangle"></i>
<span ng-bind="error"></span>
</div>
<visualize ng-if="savedVis" vis="savedVis.vis" search-source="savedVis.searchSource"></visualize>
</div>

View file

@ -1,5 +1,6 @@
@import (reference) "../../../styles/_bootstrap.less";
@import (reference) "../../../styles/theme/_theme.less";
@import (reference) "../../../styles/_mixins.less";
@import (reference) "lesshat.less";
@dashboard-background: @gray-light;
@ -71,9 +72,7 @@ dashboard-grid {
.panel-title {
font-size: inherit;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.ellipsis();
.flex(1 1 0);
}
@ -88,6 +87,20 @@ dashboard-grid {
}
}
.load-error {
text-align: center;
font-size: 1em;
.display(flex);
.flex(1 0 auto);
.justify-content(center);
.flex-direction(column);
.fa-exclamation-triangle {
font-size: 2em;
color: @btn-danger-bg;
}
}
visualize {
.flex(1, 1, 100%);
height: auto;

View file

@ -177,17 +177,20 @@ define(function (require) {
if (!resp.aggregations) return;
var aggKey = _.find(Object.keys(resp.aggregations), function (key) {
return key.substr(0, 5) === '_agg_';
});
// start merging aggregations
if (!merged.aggregations) {
merged.aggregations = {
_agg_0: {
buckets: []
}
merged.aggregations = {};
merged.aggregations[aggKey] = {
buckets: []
};
merged._bucketIndex = {};
}
resp.aggregations._agg_0.buckets.forEach(function (bucket) {
resp.aggregations[aggKey].buckets.forEach(function (bucket) {
var mbucket = merged._bucketIndex[bucket.key];
if (mbucket) {
mbucket.doc_count += bucket.doc_count;
@ -195,7 +198,7 @@ define(function (require) {
}
mbucket = merged._bucketIndex[bucket.key] = bucket;
merged.aggregations._agg_0.buckets.push(mbucket);
merged.aggregations[aggKey].buckets.push(mbucket);
});
}

View file

@ -21,8 +21,6 @@ define(function (require) {
require('apps/discover/directives/table');
require('apps/visualize/saved_visualizations/_adhoc_vis');
var app = require('modules').get('apps/discover', [
'kibana/notify',
'kibana/courier',
@ -49,8 +47,9 @@ define(function (require) {
app.controller('discover', function ($scope, config, courier, $route, $window, savedSearches, savedVisualizations,
Notifier, $location, globalState, appStateFactory, timefilter, AdhocVis, Promise, Private) {
Notifier, $location, globalState, appStateFactory, timefilter, Promise, Private) {
var Vis = Private(require('components/vis/vis'));
var segmentedFetch = $scope.segmentedFetch = Private(require('apps/discover/_segmented_fetch'));
var HitSortFn = Private(require('apps/discover/_hit_sort_fn'));
var diffTimePickerValues = Private(require('utils/diff_time_picker_vals'));
@ -127,9 +126,6 @@ define(function (require) {
indexPatternList: indexPatternList,
};
// So we can watch it.
$scope.time = timefilter.time;
// stores the complete list of fields
$scope.fields = null;
@ -153,9 +149,8 @@ define(function (require) {
if (_.difference(changed, ignoreStateChanges).length) $scope.fetch();
});
// TODO: Switch this to watching time.string when we implement it
$scope.$watchCollection('globalState.time', function (newTime, oldTime) {
if (diffTimePickerValues(newTime, oldTime)) $scope.fetch();
$scope.$listen(timefilter, 'update', function () {
$scope.fetch();
});
$scope.$watch('state.sort', function (sort) {
@ -173,7 +168,7 @@ define(function (require) {
});
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled(!!timefield);
timefilter.enabled = !!timefield;
});
// options are 'loading', 'ready', 'none', undefined
@ -591,22 +586,23 @@ define(function (require) {
// 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 shouldn't have one, or already do, return whatever we already have
if (!$scope.opts.timefield || $scope.vis) return Promise.resolve($scope.vis);
var vis = new AdhocVis({
searchSource: $scope.searchSource,
// TODO: a legit way to update the index pattern
$scope.vis = new Vis($scope.searchSource.get('index'), {
type: 'histogram',
listeners: {
onClick: function (e) {
click: function (e) {
console.log(e);
timefilter.time.from = moment(e.point.x);
timefilter.time.to = moment(e.point.x + e.data.ordered.interval);
timefilter.time.mode = 'absolute';
},
onBrush: function (e) {
brush: function (e) {
var from = moment(e.range[0]);
var to = moment(e.range[1]);
@ -617,40 +613,36 @@ define(function (require) {
timefilter.time.mode = 'absolute';
}
},
config: {
metric: {
configs: [{
agg: 'count',
}]
aggs: [
{
type: 'count',
schema: 'metric'
},
segment: {
configs: [{
agg: 'date_histogram',
{
type: 'date_histogram',
schema: 'segment',
params: {
field: $scope.opts.timefield,
interval: $state.interval,
min_doc_count: 0,
}]
},
group: { configs: [] },
split: { configs: [] },
}
interval: 'auto',
min_doc_count: 0
}
}
]
});
$scope.searchSource.aggs(function () {
return $scope.vis.aggs.toDSL();
});
// stash this promise so that other calls to setupVisualization will have to wait
loadingVis = vis.init()
.then(function () {
// expose the vis so that the visualize directive can get started
$scope.vis = vis;
// wait for visualize directive to emit that it's ready before resolving
return new Promise(function (resolve) {
$scope.$on('ready:vis', resolve);
loadingVis = new Promise(function (resolve) {
$scope.$on('ready:vis', function () {
resolve($scope.vis);
});
})
.then(function () {
.finally(function () {
// clear the loading flag
loadingVis = null;
return vis;
});
return loadingVis;

View file

@ -90,7 +90,7 @@
<div class="spinner large"> </div>
</div>
<visualize vis="vis" es-resp="mergedEsResp"></visualize>
<visualize vis="vis" es-resp="mergedEsResp" search-source="searchSource"></visualize>
</div>
<div class="discover-table"

View file

@ -1,7 +1,7 @@
<li bindonce class="sidebar-item" bo-attr bo-attr-field="field.name">
<div ng-click="toggleDetails(field)" class="sidebar-item-title">
<field-name field="field" title="{{field.name}}"></field-name>
<field-name field="field"></field-name>
<button
ng-click="toggleDisplay(field)"
bo-class="field.display ? 'btn-danger' : 'btn-primary'"

View file

@ -19,7 +19,7 @@ define(function (require, module, exports) {
sectionName: '@section'
},
link: function ($scope, $el) {
timefilter.enabled(false);
timefilter.enabled = false;
$scope.sections = require('apps/settings/sections/index');
$scope.section = _.find($scope.sections, { name: $scope.sectionName });

View file

@ -1,259 +0,0 @@
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
var ConfigTemplate = require('utils/config_template');
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
var qs = require('utils/query_string');
require('apps/visualize/saved_visualizations/saved_visualizations');
require('components/notify/notify');
require('filters/uriescape');
var app = require('modules').get('apps/visualize', [
'kibana/notify',
'kibana/courier'
]);
var visConfigCategories = require('apps/visualize/saved_visualizations/_config_categories');
require('routes')
.when('/visualize/create', {
template: require('text!apps/visualize/editor.html'),
resolve: {
vis: function (savedVisualizations, courier, $route) {
if (!$route.current.params.indexPattern && !$route.current.params.savedSearchId) {
throw new Error('You must provide either an indexPattern or a savedSearchId');
}
return savedVisualizations.get($route.current.params)
.catch(courier.redirectWhenMissing({
//'index-pattern': '/visualize',
'*': '/visualize'
}));
}
}
})
.when('/visualize/edit/:id', {
template: require('text!apps/visualize/editor.html'),
resolve: {
vis: function (savedVisualizations, courier, $route) {
return savedVisualizations.get($route.current.params.id)
.catch(courier.redirectWhenMissing({
'index-pattern': '/settings',
'*': '/visualize'
}));
}
}
});
app.controller('VisualizeEditor', function ($scope, $route, $timeout, $window, Notifier, $location,
globalState, appStateFactory, timefilter, Private) {
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
var notify = new Notifier({
location: 'Visualization Editor'
});
// get the vis loaded in from the routes
var vis = $route.current.locals.vis;
// vis.destroy called by visualize directive
var indexPattern = vis.searchSource.get('index');
$scope.fields = _.sortBy(indexPattern.fields, 'name');
$scope.fields.byName = indexPattern.fieldsByName;
var $state = $scope.state = appStateFactory.create(vis.getState());
if ($state.query) {
vis.searchSource.set('query', $state.query);
}
$scope.vis = vis;
$scope.aggs = aggs;
$scope.visConfigCategories = visConfigCategories;
var visConfigProperties = Object.keys(visConfigCategories.byName);
var init = function () {
$scope.$on('ready:vis', function () {
// once the visualization is ready, boot up
vis.setState($state);
watchForConfigChanges();
$scope.$emit('application.load');
});
};
/**
* YOU PROBABLY WANT write|readStateAndFetch
*/
var justFetch = function () {
// we use state to track query, must write before we fetch
if ($state.query) {
vis.searchSource.set('query', $state.query);
} else {
vis.searchSource.set('query', null);
}
vis.searchSource.fetch();
};
/**
* Write the latest changes made on the visualization to the $state. This
* will cause a fetch if there were changes
*
* @return {Array} - a list of the keys from state that were updated.
*/
var writeStateAndFetch = function () {
_.assign($state, vis.getState());
watchForConfigChanges();
$state.save();
justFetch();
};
/**
* Pull the state into the vis, and then fetch the searchSource
* @return {undefined}
*/
var readStateAndFetch = function () {
// update and commit the state, which will update the vis dataSource if there were new changes
vis.setState($state);
watchForConfigChanges();
justFetch();
};
var watchForConfigChanges = (function () {
var _unwatchers = [];
var _clearWatchers = function () {
_unwatchers.length && _unwatchers.splice(0).forEach(function (unwatcher) { unwatcher(); });
};
return function () {
$scope.vis.dirty = false;
_clearWatchers();
// watch config properties for deep changes
visConfigProperties.forEach(function (prop) {
_unwatchers.push($scope.$watch('vis.' + prop + '.configs', function (newVal, oldVal) {
if (newVal === oldVal) return; // stupid initRun
$scope.vis.dirty = true;
_clearWatchers();
}, true));
});
};
}());
/**
* When something else updates the state, let us know
*/
$state.onUpdate(readStateAndFetch);
/**
* Click handler for the "refresh" button
*/
$scope.doVisualize = writeStateAndFetch;
/**
* Click handler for the "new doc" button
*/
$scope.startOver = function () {
$location.url('/visualize');
};
/**
* Do that actual save, click handler for the "save" button within the save config panel
*/
$scope.doSave = function () {
writeStateAndFetch();
// use the title for the id
vis.id = vis.title;
// serialize the current state
vis.stateJSON = JSON.stringify(vis.getState());
vis.save()
.then(function () {
if (vis.id !== $route.current.params.id) {
$location.url(globalState.writeToUrl('/visualize/edit/' + encodeURIComponent(vis.id)));
}
configTemplate.close('save');
}, notify.fatal);
};
/**
* Enable the timefilter, and tell Angular to
*/
timefilter.enabled(true);
$scope.timefilter = timefilter;
// TODO: Switch this to watching time.string when we implement it
$scope.$watchCollection('timefilter.time', function (newTime, oldTime) {
// don't fetch unless there was a previous value and the values are not loosly equal
if (!_.isUndefined(oldTime) && !angular.equals(newTime, oldTime)) $scope.doVisualize();
});
// config panel templates
var configTemplate = $scope.configTemplate = new ConfigTemplate({
save: require('text!apps/visualize/partials/save.html'),
load: require('text!apps/visualize/partials/load.html'),
share: require('text!apps/visualize/partials/share.html'),
});
$scope.toggleShare = _.bindKey(configTemplate, 'toggle', 'share');
$scope.shareData = function () {
return {
link: $location.absUrl(),
// This sucks, but seems like the cleanest way. Uhg.
embed: $location.absUrl().replace('?', '?embed&')
};
};
/**
* Click handler for the "save" button.
*/
$scope.toggleSave = _.bindKey(configTemplate, 'toggle', 'save');
/**
* Toggle the load config panel
*/
$scope.toggleLoad = _.bindKey(configTemplate, 'toggle', 'load');
// objects to make available within the config panel's scope
$scope.conf = _.pick($scope, 'doSave', 'vis', 'shareData');
$scope.unlink = function () {
// display unlinking for 2 seconds, unless it is double clicked
$scope.unlinking = $timeout($scope.doneUnlinking, 2000);
delete vis.savedSearchId;
var q = vis.searchSource.get('query');
$state.query = q;
var parent = vis.searchSource.parent();
// we will copy over all state minus the "aggs"
_(parent.toJSON()).omit('aggs').forOwn(function (val, key) {
vis.searchSource.set(key, val);
});
vis.searchSource.inherits(parent.parent());
};
$scope.doneUnlinking = function () {
$scope.unlinking = clearTimeout($scope.unlinking);
$scope.linked = false;
};
$scope.linked = !!vis.savedSearchId;
if ($scope.linked) {
// possibly left over state from unsaved unlinking
delete $state.query;
} else {
var q = $state.query || vis.searchSource.get('query');
$state.query = q;
}
// init
init();
});
});

View file

@ -1,79 +0,0 @@
define(function (require) {
var module = require('modules').get('apps/visualize');
var $ = require('jquery');
var _ = require('lodash');
module.directive('visCanvas', function ($http) {
return {
restrict: 'E',
scope: {
vis: '='
},
link: function ($scope, $el) {
var $window = $(window);
var $body = $(document.body);
var vals = {
windowHeight: function () {
return $window.height();
},
offsetTop: function () {
return $el.offset().top;
}
};
var cur = {};
var needRender = function () {
var need = false;
_.forOwn(vals, function (get, name) {
var val = get();
if (cur[name] !== val) {
need = true;
cur[name] = val;
}
});
return need;
};
var render = function () {
var parentPadding = _.reduce($el.parents().toArray(), function (padding, parent) {
var $parent = $(parent);
return padding + (parseInt($parent.css('paddingBottom'), 10) || 0) - (parseInt($parent.css('marginBottom'), 10) || 0);
}, 0);
$el.css('height', cur.windowHeight - cur.offsetTop - parentPadding);
};
var poll = function () {
if (poll.id) clearTimeout(poll.id);
poll.count = 0;
(function check() {
var need = needRender();
if (need) {
poll.count = 0;
render();
} else {
poll.count ++;
}
if (poll.count < 5) poll.id = setTimeout(check, 100);
}());
};
$window.on('resize', poll);
$body.on('mousedown mouseup', poll);
$scope.pendingHttpRequests = $http.pendingRequests;
$scope.$watch('pendingHttpRequests.length', poll);
$scope.$on('$destroy', function () {
$window.off('resize', poll);
$body.off('mousedown mouseup', poll);
clearTimeout(poll.id);
});
}
};
});
});

View file

@ -1,36 +0,0 @@
define(function (require) {
var html = require('text!apps/visualize/partials/config_category.html');
require('apps/visualize/directives/config_editor');
require('modules')
.get('apps/visualize')
.directive('visConfigCategory', function (Private) {
return {
restrict: 'E',
scope: {
category: '=',
vis: '=',
fields: '='
},
template: html,
link: function ($scope, $el) {
$scope.moveHandler = function (config, delta) {
var configs = $scope.category.configs;
var i = configs.indexOf(config);
if (delta === false) {
// means remove
configs.splice(i, 1);
} else {
// move to a new position (iTarget)
var iTarget = Math.max(0, Math.min(configs.length - 1, i + delta));
if (i !== iTarget) {
configs.splice(iTarget, 0, configs.splice(i, 1).pop());
}
}
};
}
};
});
});

View file

@ -1,195 +0,0 @@
define(function (require) {
var app = require('modules').get('apps/visualize');
var _ = require('lodash');
var $ = require('jquery');
require('filters/field_type');
var visConfigCategories = require('apps/visualize/saved_visualizations/_config_categories');
var headerHtml = require('text!apps/visualize/partials/editor/header.html');
var controlHtml = {
ranges: require('text!apps/visualize/partials/controls/ranges.html'),
ip_range: require('text!apps/visualize/partials/controls/ip_range.html'),
filters: require('text!apps/visualize/partials/controls/filters.html'),
orderAndSize: require('text!apps/visualize/partials/controls/order_and_size.html'),
minDocCount: require('text!apps/visualize/partials/controls/min_doc_count.html'),
extendedBounds: require('text!apps/visualize/partials/controls/extended_bounds.html'),
interval: require('text!apps/visualize/partials/controls/interval.html'),
globalLocal: require('text!apps/visualize/partials/controls/global_local.html')
};
app.directive('visConfigEditor', function ($compile, Private) {
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
var categoryOptions = {
metric: {
template: require('text!apps/visualize/partials/editor/metric.html')
},
segment: {
template: require('text!apps/visualize/partials/editor/dimension.html'),
setup: setupDimension
},
group: {
template: require('text!apps/visualize/partials/editor/dimension.html'),
setup: setupDimension
},
split: {
template: require('text!apps/visualize/partials/editor/dimension.html'),
setup: setupDimension
}
};
// generalized setup for group and segment
function setupDimension($scope, $el) {
var $controls = $el.find('.agg-param-controls');
function getAvailableAggsForField() {
if (!$scope.config.field || !$scope.fields) return;
var field = $scope.fields.byName[$scope.config.field];
// clear the previous choices
$scope.availableAggs = void 0;
var options = [
aggs.bucketAggsByName.terms,
aggs.bucketAggsByName.histogram,
aggs.bucketAggsByName.range,
aggs.bucketAggsByName.ip_range,
aggs.bucketAggsByName.date_histogram,
aggs.bucketAggsByName.filters,
// 'range'
];
// get the new choices
//var options = aggs.byFieldType[field.type];
if (!options || options.length === 0) {
// init or invalid field type
$scope.config.agg = void 0;
return;
}
if (options.length === 1) {
// only once choice, make it for the user
$scope.config.agg = options[0].name;
return;
}
// set the new choices
$scope.availableAggs = options;
// update the agg only if it is not currently a valid option
if (!$scope.config.agg || !_.find(options, { name: $scope.config.agg })) {
$scope.config.agg = options[0].name;
return;
}
}
// since this depends on the field and field list, watch both
// this doesn't trigger when we switch the metric agg field?
$scope.$watch('config.field', function (field) {
getAvailableAggsForField(field);
if ($scope.vis && $scope.vis.searchSource) {
$scope.vis.searchSource.get('index').popularizeField(field, 1);
}
});
$scope.$watch('fields', getAvailableAggsForField);
$scope.$watch('config.agg', function (aggName) {
var agg = aggs.byName[aggName];
var controlsHtml = '';
if (agg) {
var params = $scope.aggParams = agg.params;
_.forOwn(params, function (param, name) {
// if the param doesn't have options, or a default value, skip it
if (!param.options) return;
// if there isn't currently a value, or the current value is not one of the options, set it to the default
if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
$scope.config[name] = param.default;
}
});
if (params.order && params.size && !params.order.hide) {
controlsHtml += ' ' + controlHtml.orderAndSize;
}
if (params.interval && !params.interval.hide) {
controlsHtml += ' ' + controlHtml.interval;
if (!controlsHtml.match(/aggParams\.interval\.options/)) ; //debugger;
}
if (aggName === 'range' && params.ranges) {
controlsHtml += ' ' + controlHtml.ranges;
}
if (aggName === 'ip_range' && params.ranges) {
controlsHtml += ' ' + controlHtml.ip_range;
}
if (params.filters) {
controlsHtml += ' ' + controlHtml.filters;
}
if (params.min_doc_count && !params.min_doc_count.hide) {
controlsHtml += ' ' + controlHtml.minDocCount;
}
if (params.extended_bounds && !params.extended_bounds.hide) {
controlsHtml += ' ' + controlHtml.extendedBounds;
}
if ($scope.category.name === 'group') {
controlsHtml += ' ' + controlHtml.globalLocal;
}
}
$controls.html($compile(controlsHtml)($scope));
});
}
return {
restrict: 'E',
scope: {
config: '=',
category: '=',
fields: '=',
vis: '=',
move: '='
},
link: function ($scope, $el, attr) {
$scope.aggs = aggs;
$scope.visConfigCategories = visConfigCategories;
$scope.$watch('category', function (category, prevCategory) {
// clear out the previous state if necessary
if (prevCategory && !category) {
delete $scope[category.name];
$el.html('');
return;
}
// no work to be done yet
if (!category) return;
var opts = categoryOptions[category.name];
// attach a copy of the template to the scope and render
$el.html($compile(headerHtml + '\n' + opts.template)($scope));
_.defaults($scope.val, opts.defVal || {});
if (opts.setup) opts.setup($scope, $el);
// rather than accessing vis.{{categoryName}} everywhere
$scope[category.name] = $scope.vis[category.name];
});
}
};
});
});

View file

@ -1,15 +0,0 @@
define(function (require) {
var module = require('modules').get('apps/visualize');
module.directive('visSearchEditor', function () {
return {
restrict: 'E',
scope: {
vis: '='
},
template: require('text!apps/visualize/partials/search_editor.html'),
link: function ($scope, $el) {
}
};
});
});

View file

@ -1,155 +0,0 @@
define(function (require) {
var vislib = require('components/vislib/index');
var $ = require('jquery');
var _ = require('lodash');
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
require('css!apps/visualize/styles/visualization.css');
require('apps/visualize/spy/spy');
var module = require('modules').get('kibana/directive');
module.directive('visualize', function (createNotifier, SavedVis, indexPatterns, visLib) {
return {
restrict: 'E',
scope : {
vis: '=',
esResp: '=?'
},
template: require('text!apps/visualize/partials/visualize.html'),
link: function ($scope, $el, attr) {
var chart; // set in "vis" watcher
var notify = createNotifier();
var $visualize = $el.find('.visualize-chart');
var $spy = $el.find('visualize-spy');
$scope.fields = {};
$scope.spyMode = false;
$scope.onlyShowSpy = false;
var applyClassNames = function () {
// external
$el.toggleClass('only-visualization', !$scope.spyMode);
$el.toggleClass('visualization-and-spy', $scope.spyMode && !$scope.onlyShowSpy);
$el.toggleClass('only-spy', Boolean($scope.onlyShowSpy));
$spy.toggleClass('only', Boolean($scope.onlyShowSpy));
// internal
$visualize.toggleClass('spy-visible', Boolean($scope.spyMode));
$visualize.toggleClass('spy-only', Boolean($scope.onlyShowSpy));
};
var calcResponsiveStuff = function () {
$scope.onlyShowSpy = $scope.spyMode && $el.height() < 550;
};
var render = function () {
applyClassNames();
if (chart && $scope.chartData && !$scope.onlyShowSpy) {
notify.event('call chart render', function () {
chart.render($scope.chartData);
});
}
};
// provide a setter to the visualize-spy directive
$scope.$on('change:spyMode', function (event, newMode) {
calcResponsiveStuff();
render();
});
$scope.$watch('vis', function (vis, prevVis) {
if (prevVis && vis !== prevVis && prevVis.destroy) prevVis.destroy();
if (chart) {
chart.off('hover');
chart.off('click');
chart.destroy();
}
if (!vis) return;
if (vis.error) {
$el.html('<div class="visualize-error"><i class="fa fa-exclamation-triangle"></i><br>' + vis.error + '</div>');
return;
}
var typeDefinition = typeDefs.byName[vis.typeName];
var params = {
type: vis.typeName,
};
$scope.fields = vis.searchSource.get('index').fieldsByName;
_.merge(params, vis.params);
_.defaults(params, typeDefinition.params);
chart = new visLib.Vis($visualize[0], params);
// For each type of interaction, assign the the handler if the vis object has it
// otherwise use the typeDef, otherwise, do nothing.
_.each({hover: 'onHover', click: 'onClick', brush: 'onBrush'}, function (func, event) {
var callback = vis[func] || typeDefinition[func];
if (!!callback) chart.on(event, callback);
});
if (!attr.esResp) {
// fetch the response ourselves if it's not provided
vis.searchSource.onResults(function onResults(resp) {
$scope.esResp = resp;
}).catch(notify.fatal);
}
vis.searchSource.onError(notify.error).catch(notify.fatal);
$scope.$root.$broadcast('ready:vis');
});
$scope.$watch('esResp', function (resp, prevResp) {
if (!resp) return;
var vis = $scope.vis;
var source = vis.searchSource;
$scope.chartData = vis.buildChartDataFromResponse($scope.vis.searchSource.get('index'), resp);
});
$scope.$watch('chartData', render);
$scope.$on('resize', function () {
var old;
(function waitForAnim() {
var cur = $el.width() + ':' + $el.height();
if (cur !== old) {
old = cur;
// resize can sometimes be called before animations on the element are complete.
// check each 50ms if the animations are complete and then render when they are
return setTimeout(waitForAnim, 200);
}
calcResponsiveStuff();
applyClassNames();
// chart reference changes over time, don't bind to a specific chart object.
if (chart) chart.resize();
}());
});
$scope.$on('$destroy', function () {
// Vis with missing indexpattern will not have destroy
if ($scope.vis && $scope.vis.destroy) $scope.vis.destroy();
if (chart) {
chart.off('hover');
chart.off('click');
chart.destroy();
}
});
}
};
});
});

View file

@ -1,97 +0,0 @@
<div ng-controller="VisualizeEditor" class="vis-editor">
<div ng-if="!appEmbedded" class="app-container">
<navbar>
<span ng-if="!!vis.title" class="name" ng-bind="vis.title"></span>
<div class="fill bitty-modal-container">
<div ng-if="linked && !unlinking"
ng-dblclick="unlink()"
tooltip="Double click to unlink this visualization from the saved search"
class="bitty-modal visualize-linked">
<i class="fa fa-link"></i>
&nbsp;
This visualization is linked to a saved search:
<b>{{ vis.savedSearchId | json}}</b>
<a href="#/discover/{{ vis.savedSearchId | uriescape }}">
<i class="fa fa-pencil" tooltip="Click here to edit the linked saved search"></i>
</a>
</div>
<div ng-if="linked && unlinking" ng-click="doneUnlinking()" class="bitty-modal">
<i class="fa fa-chain-broken"></i> Unlinked!
</div>
<form ng-submit="doVisualize()" class="inline-form" name="queryInput">
<div class="typeahead" kbn-typeahead="visualize">
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input query-input="vis.searchSource" input-focus
kbn-typeahead-input
placeholder="Search..."
type="text"
class="form-control"
ng-model="state.query">
<button class="btn btn-default" type="submit"
ng-disabled="queryInput.$invalid">
<span class="fa fa-search"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>
</div>
<div class="button-group">
<button ng-click="startOver()"><i class="fa fa-file-o"></i></button>
<button ng-click="toggleSave()"><i class="fa fa-save"></i></button>
<button ng-click="toggleLoad()"><i class="fa fa-folder-open"></i></button>
<button ng-click="toggleShare()"><i class="fa fa-code"></i></button>
<button ng-click="doVisualize()"><i class="fa fa-refresh"></i></button>
</div>
</navbar>
<config
config-template="configTemplate"
config-object="conf">
</config>
<div class="vis-editor-content">
<div class="vis-sidebar">
<div class="sidebar-container">
<form class="sidebar-list" ng-submit="doVisualize()" name="visualizeEditor">
<ul class="list-unstyled">
<li ng-repeat="category in visConfigCategories.displayOrder" class="sidebar-item">
<vis-config-category
vis="vis"
category="vis[category.name]"
fields="fields">
</vis-config-category>
</li>
<li class="sidebar-item">
<button
ng-click="doVisualize()"
ng-if="vis.dirty"
ng-disabled="httpActive.length || visualizeEditor.$invalid"
class="sidebar-item-button success">
Apply
</button>
</li>
</ul>
</form>
</div>
</div>
<div class="vis-canvas">
<vis-canvas><visualize vis="vis"></visualize></vis-canvas>
</div>
</div>
</div>
<div ng-if="appEmbedded" class="container-fluid">
<div class="row vis-editor-content">
<div class="vis-canvas">
<vis-canvas><visualize vis="vis"></visualize></vis-canvas>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,79 @@
<ng-form name="aggForm" class="vis-editor-agg">
<!-- header -->
<div class="vis-editor-agg-header">
<!-- open/close editor -->
<button
ng-click="editorOpen = !editorOpen"
type="button"
class="btn btn-xs vis-editor-agg-header-toggle">
<i ng-class="{ 'fa-caret-down': editorOpen, 'fa-caret-right': !editorOpen }" class="fa"></i>
</button>
<!-- 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">
{{ describeError() }}
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<button
ng-if="group.length > 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 class="fa fa-caret-up"></i>
</button>
<button
ng-if="group.length > 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 class="fa fa-caret-down"></i>
</button>
<button
ng-if="group.length > groupMin"
ng-click="remove(agg)"
tooltip="Remove Dimension"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-danger">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="vis-editor-agg-editor" ng-show="editorOpen">
<!-- schema editors go here -->
<div class="form-group">
<label>Aggregation</label>
<select
name="agg"
class="form-control"
ng-model="agg.type"
required
ng-options="agg as agg.title for agg in aggTypeOptions">
</select>
</div>
</div>
</ng-form>

View file

@ -0,0 +1,163 @@
define(function (require) {
require('modules')
.get('app/visualize')
.directive('visEditorAgg', function ($compile, $parse, Private, Notifier) {
var _ = require('lodash');
var $ = require('jquery');
var aggTypes = Private(require('components/agg_types/index'));
require('apps/visualize/editor/agg_param');
var notify = new Notifier({
location: 'visAggGroup'
});
return {
restrict: 'E',
replace: true,
template: require('text!apps/visualize/editor/agg.html'),
scope: {
vis: '=',
agg: '=',
$index: '=',
group: '=',
groupName: '=',
groupMin: '='
},
link: function ($scope, $el) {
$scope.aggTypeOptions = aggTypes.byType[$scope.groupName];
$scope.editorOpen = $scope.agg.brandNew;
$scope.$watch('$index', function (i) {
$scope.$first = i === 0;
$scope.$last = i === $scope.group.length - 1;
});
(function setupControlManagement() {
var $editorContainer = $el.find('.vis-editor-agg-editor');
if ($scope.agg.schema.editor) {
var $schemaEditor = $('<div>').prependTo($editorContainer);
$schemaEditor.append($scope.agg.schema.editor);
$compile($schemaEditor)(editorScope());
}
var $aggParamEditors;
var $aggParamEditorsScope;
$scope.$watch('agg.type', function updateAggParamEditor() {
if ($aggParamEditors) {
$aggParamEditors.remove();
$aggParamEditorsScope.$destroy();
$aggParamEditors = $aggParamEditorsScope = null;
}
var agg = $scope.agg;
var type = $scope.agg.type;
if (!agg) return;
agg.fillDefaults();
if (!type) return;
var editors = type.params.map(function (param, i) {
if (!param.editor) return;
return $('<vis-agg-param-editor>')
.attr({
'agg-type': 'agg.type',
'agg-config': 'agg',
'agg-param': 'agg.type.params[' + i + ']',
'params': 'agg.params'
})
.append(param.editor)
.get(0);
}).filter(Boolean);
$aggParamEditors = $(editors).appendTo($editorContainer);
$aggParamEditorsScope = $scope.$new();
$compile($aggParamEditors)($aggParamEditorsScope);
});
// generic child scope creation, for both schema and agg
function editorScope() {
var $editorScope = $scope.$new();
setupBoundProp($editorScope, 'agg.type', 'aggType');
setupBoundProp($editorScope, 'agg', 'aggConfig');
setupBoundProp($editorScope, 'agg.params', 'params');
return $editorScope;
}
// bind a property from our scope a child scope, with one-way binding
function setupBoundProp($child, get, set) {
var getter = _.partial($parse(get), $scope);
var setter = _.partial($parse(set).assign, $child);
$scope.$watch(getter, setter);
}
}());
/**
* Describe the aggregation, for display in the collapsed agg header
* @return {[type]} [description]
*/
$scope.describe = function () {
if (!$scope.agg.type.makeLabel) return '';
var label = $scope.agg.type.makeLabel($scope.agg);
return label ? label : '';
};
/**
* Describe the errors in this agg
* @return {[type]} [description]
*/
$scope.describeError = function () {
var count = _.reduce($scope.aggForm.$error, function (count, controls, errorType) {
return count + _.size(controls);
}, 0);
return count + ' Error' + (count > 1 ? 's' : '');
};
$scope.moveUp = function (agg) {
var aggs = $scope.vis.aggs;
var i = aggs.indexOf(agg);
if (i <= 0) return notify.log('already first');
aggs.splice(i, 1);
// find the most previous bucket agg
var d = i - 1;
for (; d > 0 && aggs[d].schema.group !== 'buckets'; d--) ;
// place this right before
aggs.splice(d, 0, agg);
};
$scope.moveDown = function (agg) {
var aggs = $scope.vis.aggs;
var i = aggs.indexOf(agg);
if (i >= aggs.length - 1) return notify.log('already last');
aggs.splice(i, 1);
// find the next bucket agg
var d = i;
for (; d < aggs.length && aggs[d].schema.group !== 'buckets'; d++) ;
// place this agg right after
aggs.splice(d + 1, 0, agg);
};
$scope.remove = function (agg) {
var aggs = $scope.vis.aggs;
var index = aggs.indexOf(agg);
if (index === -1) return notify.log('already removed');
aggs.splice(index, 1);
};
}
};
});
});

View file

@ -0,0 +1,48 @@
<li class="sidebar-item">
<div class="sidebar-item-title">
{{ groupName }}
</div>
<div class="vis-editor-agg-group" ng-class="groupName">
<!-- wrapper needed for nesting-indicator -->
<div ng-repeat="agg in group" class="vis-editor-agg-wrapper">
<nesting-indicator
ng-if="groupName === 'buckets'"
item="agg"
index="$index"
list="group">
</nesting-indicator>
<!-- agg.html -->
<vis-editor-agg
vis="vis"
group="group"
group-name="groupName"
group-min="stats.min"
$index="$index"
agg="agg">
</vis-editor-agg>
</div>
<div ng-if="addForm.visible" class="vis-editor-agg-add-form form-group">
<label>Select {{ groupName }} type</label>
<button
ng-repeat="schema in availableSchema"
ng-click="createUsingSchema(schema)"
class="btn-default">
<i ng-show="schema.icon" ng-class="schema.icon"></i>
{{schema.title}}
</button>
</div>
<button
ng-if="stats.count < stats.max"
type="button"
ng-click="addForm.visible = !addForm.visible"
class="vis-editor-agg-wide-btn btn btn-xs btn-default" >
<i ng-if="!addForm.visible" class="fa fa-plus"></i>
<span ng-if="addForm.visible">cancel</span>
</button>
</div>
</li>

View file

@ -0,0 +1,72 @@
define(function (require) {
require('modules')
.get('app/visualize')
.directive('visEditorAggGroup', function (Private) {
require('apps/visualize/editor/agg');
require('apps/visualize/editor/nesting_indicator');
var eachGroupHtml = require('text!apps/visualize/editor/agg_group.html');
var AggConfig = Private(require('components/vis/_agg_config'));
return {
restrict: 'E',
template: require('text!apps/visualize/editor/agg_group.html'),
replace: true,
scope: {
vis: '=',
schemas: '=',
group: '=',
groupName: '='
},
link: function ($scope) {
// "sub-scope" for the add form to use
$scope.addForm = {};
$scope.$watchMulti([
'schemas',
'group.length'
], function () {
var stats = $scope.stats = {
min: 0,
max: 0,
count: $scope.group ? $scope.group.length : 0
};
if (!$scope.schemas) return;
$scope.schemas.forEach(function (schema) {
stats.min += schema.min;
stats.max += schema.max;
});
});
$scope.$watchCollection('group', function () {
$scope.availableSchema = $scope.schemas.filter(function (schema) {
var count = 0;
if ($scope.group) {
count = $scope.group.reduce(function (count, aggConfig) {
if (aggConfig.schema === schema) count += 1;
return count;
}, 0);
}
if (count < schema.max) return true;
});
});
$scope.createUsingSchema = function (schema) {
$scope.addForm = {};
var aggConfig = new AggConfig($scope.vis, {
schema: schema
});
aggConfig.brandNew = true;
$scope.vis.aggs.push(aggConfig);
};
}
};
});
});

View file

@ -0,0 +1,18 @@
define(function (require) {
require('modules')
.get('app/visualize')
.directive('visAggParamEditor', function () {
return {
restrict: 'E',
scope: {
aggType: '=',
aggConfig: '=',
aggParam: '=',
params: '='
},
template: function ($el, attr) {
return $el.html();
}
};
});
});

View file

@ -0,0 +1,87 @@
<div ng-controller="VisEditor" class="vis-editor">
<navbar ng-if="!appEmbedded">
<div class="fill bitty-modal-container">
<div ng-if="linked && !unlinking"
ng-dblclick="unlink()"
tooltip="Double click to unlink this visualization from the saved search"
class="bitty-modal visualize-linked">
<i class="fa fa-link"></i>
&nbsp;
This visualization is linked to a saved search:
<b>{{ savedVis.savedSearchId | json}}</b>
<a href="#/discover/{{ savedVis.savedSearchId | uriescape }}">
<i class="fa fa-pencil" tooltip="Click here to edit the linked saved search"></i>
</a>
</div>
<div ng-if="linked && unlinking" ng-click="doneUnlinking()" class="bitty-modal">
<i class="fa fa-chain-broken"></i> Unlinked!
</div>
<form ng-submit="fetch()" class="inline-form" name="queryInput">
<div class="typeahead" kbn-typeahead="visualize">
<div class="input-group"
ng-class="queryInput.$invalid ? 'has-error' : ''">
<input
query-input="savedVis.searchSource"
input-focus
kbn-typeahead-input
placeholder="Search..."
type="text"
class="form-control"
ng-model="state.query">
<button
class="btn btn-default" type="submit"
ng-disabled="queryInput.$invalid">
<span class="fa fa-search"></span>
</button>
</div>
<kbn-typeahead-items></kbn-typeahead-items>
</div>
</form>
</div>
<div class="button-group">
<button ng-click="startOver()"><i class="fa fa-file-o"></i></button>
<!-- normal save -->
<button ng-if="!editableVis.dirty" ng-click="toggleSave()">
<i class="fa fa-save"></i>
</button>
<!-- save stub with tooltip -->
<button disabled ng-if="editableVis.dirty" tooltip="Apply or Discard your changes before saving">
<i class="fa fa-save"></i>
</button>
<button ng-click="toggleLoad()"><i class="fa fa-folder-open"></i></button>
<button ng-click="toggleShare()"><i class="fa fa-code"></i></button>
<button ng-click="fetch()"><i class="fa fa-refresh"></i></button>
</div>
</navbar>
<config
ng-if="!appEmbedded"
config-template="configTemplate"
config-object="conf">
</config>
<div class="vis-editor-content">
<vis-editor-sidebar
ng-if="!appEmbedded"
vis="editableVis"
apply="stageEditableVis(); fetch();"
reset="resetEditableVis();">
</vis-editor-sidebar>
<div class="vis-editor-canvas">
<p class="vis-editor-canvas-title" ng-if="savedVis.title" ng-bind="savedVis.title"></p>
<visualize vis="vis" search-source="savedVis.searchSource"></visualize>
</div>
</div>
</div>

View file

@ -0,0 +1,208 @@
define(function (require) {
require('apps/visualize/saved_visualizations/saved_visualizations');
require('apps/visualize/editor/sidebar');
require('directives/saved_object_finder');
require('components/visualize/visualize');
require('filters/uriescape');
require('routes')
.when('/visualize/create', {
template: require('text!apps/visualize/editor/editor.html'),
resolve: {
savedVis: function (savedVisualizations, courier, $route) {
if (!$route.current.params.indexPattern && !$route.current.params.savedSearchId) {
throw new Error('You must provide either an indexPattern or a savedSearchId');
}
return savedVisualizations.get($route.current.params)
.catch(courier.redirectWhenMissing({
//'index-pattern': '/visualize',
'*': '/visualize'
}));
}
}
})
.when('/visualize/edit/:id', {
template: require('text!apps/visualize/editor/editor.html'),
resolve: {
savedVis: function (savedVisualizations, courier, $route) {
return savedVisualizations.get($route.current.params.id)
.catch(courier.redirectWhenMissing({
'index-pattern': '/settings',
'*': '/visualize'
}));
}
}
});
require('modules')
.get('app/visualize', [
'kibana/notify',
'kibana/courier'
])
.controller('VisEditor', function ($scope, $route, timefilter, appStateFactory, $location, globalState, $timeout) {
var _ = require('lodash');
var angular = require('angular');
var ConfigTemplate = require('utils/config_template');
var Notifier = require('components/notify/_notifier');
var notify = new Notifier({
location: 'Visualization Editor'
});
var savedVis = $route.current.locals.savedVis;
var vis = savedVis.vis;
var editableVis = vis.clone();
var searchSource = savedVis.searchSource;
// config panel templates
var configTemplate = new ConfigTemplate({
save: require('text!apps/visualize/editor/panels/save.html'),
load: require('text!apps/visualize/editor/panels/load.html'),
share: require('text!apps/visualize/editor/panels/share.html'),
});
var $state = (function initState() {
var savedVisState = vis.getState();
var $state = appStateFactory.create({
vis: savedVisState
});
if (!angular.equals($state.vis, savedVisState)) {
vis.setState($state.vis);
editableVis.setState($state.vis);
}
return $state;
}());
function init() {
// export some objects
$scope.savedVis = savedVis;
$scope.vis = vis;
$scope.editableVis = editableVis;
$scope.state = $state;
$scope.conf = _.pick($scope, 'doSave', 'savedVis', 'shareData');
$scope.configTemplate = configTemplate;
$scope.toggleShare = _.bindKey(configTemplate, 'toggle', 'share');
$scope.toggleSave = _.bindKey(configTemplate, 'toggle', 'save');
$scope.toggleLoad = _.bindKey(configTemplate, 'toggle', 'load');
$scope.linked = !!savedVis.savedSearchId;
if ($scope.linked) {
// possibly left over state from unsaved unlinking
delete $state.query;
} else {
$state.query = $state.query || searchSource.get('query');
searchSource.set('query', $state.query);
}
// track state of editable vis vs. "actual" vis
$scope.stageEditableVis = transferVisState(editableVis, vis);
$scope.resetEditableVis = transferVisState(vis, editableVis);
$scope.$watch(function () {
return editableVis.getState();
}, function (newState) {
editableVis.dirty = !angular.equals(newState, vis.getState());
}, true);
$state.on('fetch_with_changes', function () {
vis.setState($state.vis);
editableVis.setState($state.vis);
// we use state to track query, must write before we fetch
if ($state.query) {
searchSource.set('query', $state.query);
} else {
searchSource.set('query', null);
}
$scope.fetch();
});
timefilter.enabled = true;
timefilter.on('update', _.bindKey($scope, 'fetch'));
$scope.$on('ready:vis', function () {
$scope.$emit('application.load');
});
$scope.$on('$destroy', function () {
savedVis.destroy();
});
}
$scope.fetch = function () {
searchSource.fetch();
};
$scope.startOver = function () {
$location.url('/visualize');
};
$scope.doSave = function () {
savedVis.id = savedVis.title;
savedVis.visState = $state.vis;
savedVis.save()
.then(function () {
configTemplate.close('save');
notify.info('Saved Visualization "' + savedVis.title + '"');
if (savedVis.id === $route.current.params.id) return;
$location.url(
globalState.writeToUrl(
'/visualize/edit/' + encodeURIComponent(savedVis.id)
)
);
}, notify.fatal);
};
$scope.shareData = function () {
return {
link: $location.absUrl(),
// This sucks, but seems like the cleanest way. Uhg.
embed: $location.absUrl().replace('?', '?embed&')
};
};
$scope.unlink = function () {
// display unlinking for 2 seconds, unless it is double clicked
$scope.unlinking = $timeout($scope.doneUnlinking, 2000);
delete savedVis.savedSearchId;
var q = searchSource.get('query');
$state.query = q;
var parent = searchSource.parent();
// we will copy over all state minus the "aggs"
_(parent.toJSON()).omit('aggs').forOwn(function (val, key) {
searchSource.set(key, val);
});
searchSource.inherits(parent.parent());
};
$scope.doneUnlinking = function () {
$scope.unlinking = clearTimeout($scope.unlinking);
$scope.linked = false;
};
function transferVisState(fromVis, toVis) {
return function () {
toVis.setState(fromVis.getState());
editableVis.dirty = false;
$state.vis = vis.getState();
$state.save();
};
}
init();
});
});

View file

@ -0,0 +1,202 @@
.vis-editor {
.flex-parent();
navbar {
.bitty-modal-container {
position: relative;
.bitty-modal {
position: absolute;
cursor: pointer;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 10;
background: rgba(70, 82, 93, 0.9);
color: white;
text-align: center;
padding-top: 6px;
.user-select(none);
}
}
}
&-content {
.flex-parent();
// overrides for tablet and desktop
@media (min-width: @screen-md-min) {
.flex-direction(row);
}
}
&-sidebar {
.flex-parent(0, 0, auto);
overflow: auto;
// overrided for tablet and desktop
@media (min-width: @screen-md-min) {
.flex-basis(@vis-editor-sidebar-basis);
min-width: @vis-editor-sidebar-min-width;
}
.sidebar-container {
.flex(1, 0, auto);
background-color: @body-bg;
border-right-color: @sidebar-bg;
}
.sidebar-item-title {
background: @sidebar-bg;
}
.sidebar-item-title:hover {
color: @sidebar-header-color !important;
background-color: @sidebar-bg !important;
}
}
nesting-indicator {
.display(flex);
.flex(0 0 auto);
> span {
width: @vis-editor-nesting-width;
background-color: @brand-success;
.transition(width .3s ease-out);
&.expand {
width: @vis-editor-nesting-expand-width;
}
}
}
&-agg {
.flex-parent();
padding: @vis-editor-agg-editor-spacing;
border-bottom: 1px solid @sidebar-bg;
// wraps the .vis-editor-agg and nesting-indicator ^^
&-wrapper {
.display(flex);
}
&-group {
.flex-parent();
color: @text-color;
margin-bottom: 10px;
}
&-header {
.display(flex);
.align-items(center);
.flex(1);
&-toggle {
.flex(0, 0, auto);
margin-right: @vis-editor-agg-editor-spacing;
}
&-title {
.flex(1, 1, auto);
.ellipsis();
font-weight: bold;
}
&-description {
font-weight: normal;
padding-right: @vis-editor-agg-editor-spacing;
&.danger {
.text-danger();
font-weight: bold;
}
}
&-controls {
.flex(0, 0, auto);
}
}
&-editor {
margin-top: @vis-editor-agg-editor-spacing;
&-ranges {
td {
padding: 0 @vis-editor-agg-editor-spacing @vis-editor-agg-editor-spacing 0;
&:last-child {
padding-right: 0;
}
}
}
}
&-form-row {
.display(flex);
> * {
.flex(1, 1, auto);
margin-right: @vis-editor-agg-editor-spacing;
&:last-child {
margin-right: 0px;
}
}
> .btn {
.align-self(center);
}
}
&-wide-btn {
.border-radius(0);
}
&-add-form {
margin: @vis-editor-agg-editor-spacing * 3;
padding: @vis-editor-agg-editor-spacing;
> button {
display: block;
text-align: left;
width: 100%;
margin: 0 0 5px 0;
}
}
}
&-canvas {
.flex(1, 0, @screen-md-min - @vis-editor-sidebar-basis);
.display(flex);
.flex-direction(column);
overflow: auto;
// overrided for tablet and desktop
@media (min-width: @screen-md-min) {
.flex-shrink(1);
.flex-basis(100%);
}
&-title {
text-align: center;
margin: 10px 0 0;
}
visualize {
.flex-parent();
.flex(1, 0, auto);
}
.visualize-chart {
.flex(1, 0, 100%);
position: relative;
}
}
}
form.vis-share {
div.form-control {
height: inherit;
}
}

View file

@ -0,0 +1,90 @@
define(function (require) {
require('modules')
.get('kibana')
.directive('nestingIndicator', function ($rootScope, $parse) {
var _ = require('lodash');
var angular = require('angular');
var ruleBase = 'border-left-';
var getColor = (function () {
var i = 0;
var colorPool = require('components/vislib/utils/colorspace')(100);
var assigned = {};
return function (item) {
var key = item.$$hashKey;
if (!key) throw new Error('expected an item that is part of an ngRepeat');
if (!assigned[key]) {
assigned[key] = colorPool[i++ % colorPool.length];
}
return assigned[key];
};
}());
var allIndicators = [];
allIndicators.expanded = false;
allIndicators.expand = toggler(true);
allIndicators.contract = toggler(false, 150);
function toggler(on, delay) {
var all = allIndicators;
var work = function () {
if (delay && all.expanded !== on) return;
all.forEach(function ($scope) {
if (!$scope.bars) return;
$scope.bars.forEach(function ($el) {
$el.toggleClass('expand', on);
});
});
};
return function () {
all.expanded = on;
if (!delay) work();
else setTimeout(work, delay);
};
}
return {
restrict: 'E',
scope: {
item: '=',
list: '='
},
link: function ($scope, $el, attr) {
allIndicators.push($scope);
$el.on('mouseenter', allIndicators.expand);
$el.on('mouseleave', allIndicators.contract);
$scope.$on('$destroy', function () {
_.pull(allIndicators, $scope);
$el.off('mouseenter', allIndicators.expand);
$el.off('mouseleave', allIndicators.contract);
});
$scope.$watchCollection('list', function () {
if (!$scope.list || !$scope.item) return;
var item = $scope.item;
var list = $scope.list;
var bars = $scope.bars = [];
for (var i = 0; i <= list.length; i++) {
var color = getColor(list[i]);
bars.push(
angular
.element('<span>')
.css('background-color', color)
);
if (list[i] === $scope.item) break;
}
$el.html(bars);
});
}
};
});
});

View file

@ -1,11 +1,11 @@
<form role="form" ng-submit="conf.doSave()">
<div class="form-group">
<label for="visTitle">Title</label>
<input class="form-control" type="text" name="visTitle" ng-model="conf.vis.title" required input-focus>
<input class="form-control" type="text" name="visTitle" ng-model="conf.savedVis.title" required>
</div>
<div class="form-group">
<label for="visDescription">Description</label>
<textarea class="form-control" name="visDescription" ng-model="conf.vis.description" placeholder=""></textarea>
<textarea class="form-control" name="visDescription" ng-model="conf.savedVis.description" placeholder=""></textarea>
</div>
<button type="submit" class="btn btn-primary">Save</button>

View file

@ -0,0 +1,11 @@
<form role="form" class="vis-share">
<div class="form-group">
<label>Embed this visualization. <small>Copy code into your html source. Note all clients must still be able to access kibana</small></label>
<div class="form-control" disabled>&lt;iframe src="{{conf.shareData().embed}}" height="400" width="600"&gt;&lt;/iframe&gt;</div>
</div>
<div class="form-group">
<label>Share a link</label>
<div class="form-control" disabled>{{conf.shareData().link}}</div>
</div>
</form>

View file

@ -0,0 +1,46 @@
<div class="vis-editor-sidebar">
<div class="sidebar-container">
<form class="sidebar-list" ng-submit="apply()" name="visualizeEditor">
<ul class="list-unstyled">
<!-- metrics -->
<vis-editor-agg-group
ng-if="vis.type.schemas.metrics"
vis="vis"
saved-vis="savedVis"
schemas="vis.type.schemas.metrics"
group="vis.aggs.bySchemaGroup.metrics"
group-name="'metrics'"
>
</vis-editor-agg-group>
<!-- buckets -->
<vis-editor-agg-group
ng-if="vis.type.schemas.buckets"
vis="vis"
saved-vis="savedVis"
schemas="vis.type.schemas.buckets"
group="vis.aggs.bySchemaGroup.buckets"
group-name="'buckets'"
>
</vis-editor-agg-group>
<!-- apply/discard -->
<li ng-if="vis.dirty" class="sidebar-item">
<button
type="submit"
ng-disabled="httpActive.length || visualizeEditor.$invalid"
class="sidebar-item-button success">
Apply
</button>
<button
type="button"
ng-click="reset()"
class="sidebar-item-button warn">
Discard
</button>
</li>
</ul>
</form>
</div>
</div>

View file

@ -0,0 +1,21 @@
define(function (require) {
require('modules')
.get('app/visualize')
.directive('visEditorSidebar', function () {
var _ = require('lodash');
require('apps/visualize/editor/agg_group');
return {
restrict: 'E',
template: require('text!apps/visualize/editor/sidebar.html'),
replace: true,
scope: {
vis: '=',
savedVis: '=',
apply: '&',
reset: '&'
}
};
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -1,13 +1,8 @@
define(function (require) {
require('css!apps/visualize/styles/main.css');
require('apps/visualize/controllers/editor');
require('apps/visualize/controllers/wizard');
require('apps/visualize/directives/canvas');
require('apps/visualize/directives/visualize');
require('apps/visualize/directives/config_category');
require('apps/visualize/directives/search_editor');
require('apps/visualize/editor/editor');
require('apps/visualize/wizard/wizard');
require('routes')
.when('/visualize', {

View file

@ -1,24 +0,0 @@
<div
class="sidebar-item-title"
ng-if="category.name !== 'group' || vis.segment.configs.length > 0">
<span>
<button
type="button"
ng-if="category.configs.length < category.max"
ng-click="vis.addConfig(category.name)"
class="btn btn-xs btn-default" >
<i class="fa fa-plus"></i>
</button>
{{ category.label }}
</span>
</div>
<div ng-if="category.configs.length > 0" class="vis-config-details">
<vis-config-editor
ng-repeat="config in category.configs"
category="category"
config="config"
vis="vis"
fields="fields"
move="moveHandler">
</vis-config-editor>
</div>

View file

@ -1,22 +0,0 @@
<div class="form-group">
<div class="row">
<div class="col-xs-6">
<label>Min <small>(optional)</small></label>
<input
ng-show="aggParams.min_doc_count"
ng-model="config.extended_bounds.min"
type="number"
class="form-control"
name="extended_bounds.min" />
</div>
<div class="col-xs-6">
<label>Max <small>(optional)</small></label>
<input
ng-show="aggParams.min_doc_count"
ng-model="config.extended_bounds.max"
type="number"
class="form-control"
name="extended_bounds.max" />
</div>
</div>
</div>

View file

@ -1,26 +0,0 @@
<div ng-init="config.filters = (config.filters || [{input: {}}])">
<div class="form-group" >
<div ng-repeat="filter in config.filters track by $index">
<div class="config-controls pull-right btn-group">
<span ng-click="config.filters.splice($index, 1)"
class="btn btn-danger btn-xs ">
<i class="fa fa-ban" ></i>
</span>
</div>
<div class="form-group">
<label>Query string {{$index + 1}}</label>
<input query-input
ng-model="filter.input"
type="text"
class="form-control"
name="filter{{$index}}" />
</div>
</div>
</div>
</div>
<div
ng-click="config.filters.push({input: {}})"
class="sidebar-item-button primary">
Add filter
</div>
</div>

View file

@ -1,9 +0,0 @@
<div class="checkbox">
<label>
<input
ng-model="config.global"
type="checkbox">Run First
&nbsp;
<kbn-info info="Ensure that all charts have the same {{group.label | lowercase}} values." placement="right"></kbn-info>
</label>
</div>

View file

@ -1,22 +0,0 @@
<div class="form-group">
<table class="agg-config-interval">
<tr>
<td>
<label>Interval</label>
<select
ng-if="aggParams.interval.options"
ng-model="config.interval"
ng-options="opt.val as opt.display for opt in aggParams.interval.options"
class="form-control"
name="interval">
</select>
<input
ng-if="!aggParams.interval.options"
ng-model="config.interval"
type="number"
class="form-control"
name="interval" />
</td>
</tr>
</table>
</div>

View file

@ -1,43 +0,0 @@
<div ng-init="config.ranges = (config.ranges || [{}])">
<div class="form-group" >
<div class="row">
<div class="col-xs-5">
<label>From</label>
</div>
<div class="col-xs-5">
<label>To</label>
</div>
</div>
<div class="row"
ng-repeat="range in config.ranges track by $index">
<div class="col-xs-5">
<input validate-ip
ng-model="range.from"
type="text"
class="form-control"
name="range.from" />
</div>
<div class="col-xs-5">
<input validate-ip
ng-model="range.to"
type="text"
class="form-control"
name="range.to" />
</div>
<div class="col-xs-1">
<button ng-click="config.ranges.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
</button>
</div>
</div>
</div>
<div
ng-click="config.ranges.push({})"
class="sidebar-item-button primary">
Add Range
</div>
</div>

View file

@ -1,16 +0,0 @@
<div class="form-group">
<table class="agg-config-interval">
<tr>
<td>
<div class="checkbox ng-scope">
<label>
<input ng-model="config.min_doc_count"
type="checkbox">Show empty buckets
&nbsp;
<kbn-info info="Show all buckets, not only the buckets with results." placement="right" class="ng-isolate-scope"></kbn-info>
</label>
</div>
</td>
</tr>
</table>
</div>

View file

@ -1,15 +0,0 @@
<div class="form-group">
<div class="row">
<div class="col-sm-6">
<select
class="form-control"
name="order"
ng-model="config.order"
ng-options="opt.val as opt.display for opt in aggParams.order.options">
</select>
</div>
<div class="col-sm-6">
<input class="form-control" type="number" ng-model="config.size" name="size">
</div>
</div>
</div>

View file

@ -1,43 +0,0 @@
<div ng-init="config.ranges = (config.ranges || [{}])">
<div class="form-group" >
<div class="row">
<div class="col-xs-5">
<label>From</label>
</div>
<div class="col-xs-5">
<label>To</label>
</div>
</div>
<div class="row"
ng-repeat="range in config.ranges track by $index">
<div class="col-xs-5">
<input
ng-model="range.from"
type="number"
class="form-control"
name="range.from" />
</div>
<div class="col-xs-5">
<input
ng-model="range.to"
type="number"
class="form-control"
name="range.to" />
</div>
<div class="col-xs-1">
<button ng-click="config.ranges.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
</button>
</div>
</div>
</div>
<div
ng-click="config.ranges.push({})"
class="sidebar-item-button primary">
Add Range
</div>
</div>

View file

@ -1,39 +0,0 @@
<div ng-if="category.name === 'split'" class="form-group">
<div class="btn-group">
<button
type="button"
class="btn btn-xs btn-default"
ng-model="config.row"
btn-radio="true">
Row
</button>
<button
type="button"
class="btn btn-xs btn-default"
ng-model="config.row"
btn-radio="false">
Column
</button>
</div>
</div>
<div class="form-group" ng-show="availableAggs">
<label>Aggregation</label>
<select
name="agg"
class="form-control"
ng-model="config.agg"
ng-options="agg.name as agg.display for agg in availableAggs">
</select>
</div>
<div class="form-group">
<label for="field">
Field
</label>
<select
class="form-control"
name="field"
ng-model="config.field"
ng-options="field.name as field.name group by field.type for field in fields | filter:{indexed:true} |orderBy:['type','name']">
</select>
</div>
<div class="agg-param-controls"></div>

View file

@ -1,25 +0,0 @@
<div class="config-controls pull-right btn-group">
<button
ng-if="category.configs.length > 1"
ng-click="move(config, -1)"
type="button"
ng-disabled="category.configs.indexOf(config) < 1"
class="btn btn-xs btn-default">
<div tooltip="Increase Priority"><i class="fa fa-caret-up"></i></div>
</button>
<button
ng-if="category.configs.length > 1"
ng-click="move(config, 1)"
type="button"
ng-disabled="category.configs.indexOf(config) >= category.configs.length - 1"
class="btn btn-xs btn-default">
<div tooltip="Decrease Priority"><i class="fa fa-caret-down"></i></div>
</button>
<button
ng-if="category.configs.length > category.min"
ng-click="move(config, false)"
type="button"
class="btn btn-xs btn-danger">
<div tooltip="Remove Dimension"><i class="fa fa-times"></i></div>
</button>
</div>

View file

@ -1,21 +0,0 @@
<div class="form-group">
<label for="stat">Stat</label>
<select
class="form-control"
name="stat"
ng-model="config.agg"
ng-options="agg.name as agg.name for agg in aggs.metricAggs">
</select>
</div>
<div class="form-group" ng-if="config.agg && config.agg !== 'count'">
<label for="field">
Field to {{config.agg}}&nbsp;
<kbn-info placement="right" info="Field to use for the {{metric.label}}"></kbn-info>
</label>
<select
class="form-control"
name="field"
ng-model="config.field"
ng-options="field.name as field.name for field in fields | fieldType:aggs.metricAggsByName[config.agg].types">
</select>
</div>

View file

@ -1,11 +0,0 @@
<form role="form" class="vis-share">
<div class="form-group">
<label>Embed this visualization. <small>Copy code into your html source. Note all clients must still be able to access kibana</small></label>
<div class="form-control" disabled>&lt;iframe src="{{conf.shareData().embed}}" height="400" width="600"&gt;&lt;/iframe&gt;</div>
</div>
<div class="form-group">
<label>Share a link</label>
<div class="form-control" disabled>{{conf.shareData().link}}</div>
</div>
</form>

View file

@ -1,131 +0,0 @@
define(function (require) {
var _ = require('lodash');
var module = require('modules').get('app/visualize');
var configCats = require('apps/visualize/saved_visualizations/_config_categories');
module.factory('AdhocVis', function (courier, Private, Promise) {
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
/**
opts params:
{
type: 'histogram', // The chart type
listeners : {
onClick: function,
onHover: function,
onBrush: function,
},
params: {}, // top level chart parameters
searchSource: SearchSource // the search source for the visualization
}
*/
function AdhocVis(opts) {
opts = opts || {};
if (!_.isObject(opts)) throw new TypeError('options must be an object');
var vis = this;
var params;
var createdSource = true;
vis.init = _.once(function () {
vis.typeName = opts.type || 'histogram';
vis.params = _.cloneDeep(opts.params);
// give vis the properties of config
_.assign(vis, opts.config);
// also give it the on* interaction functions, if any
_.assign(vis, opts.listeners);
vis._fillConfigsToMinimum();
// resolve the search source for this AdhocVis
return Promise.cast((function () {
if (opts.searchSource) {
// did not create the source, so we won't destroy it either
createdSource = false;
return opts.searchSource;
}
return courier.createSource('search');
}()))
.then(function (searchSource) {
// TODO: Should we abtract out the agg building stuff?
searchSource.aggs(function () {
// stores the config objects in queryDsl
var dsl = {};
// counter to ensure unique agg names
var i = 0;
// start at the root, but the current will move
var current = dsl;
// continue to nest the aggs under each other
// writes to the dsl object
vis.getConfig().forEach(function (config) {
current.aggs = {};
var key = '_agg_' + (i++);
var aggDsl = {};
aggDsl[config.agg] = config.aggParams;
current = current.aggs[key] = aggDsl;
});
// set the dsl to the searchSource
return dsl.aggs || {};
});
vis.searchSource = searchSource;
return vis;
});
});
// TODO: Should this be abstracted somewhere? Its a copy/paste from _saved_vis.js
vis._fillConfigsToMinimum = function () {
// satify the min count for each category
configCats.fetchOrder.forEach(function (category) {
var myCat = vis[category.name];
if (myCat.configs.length < myCat.min) {
_.times(myCat.min - myCat.configs.length, function () {
vis.addConfig(category.name);
});
}
});
};
vis.destroy = function () {
if (createdSource) {
this.searchSource.cancelPending();
} else {
//remove our aggregations from the serarch source
this.searchSource.set('aggs', null);
}
};
/**
* Create a list of config objects, which are ready to be turned into aggregations,
* in the order which they should be executed.
*
* @return {Array} - The list of config objects
*/
vis.getConfig = Private(require('apps/visualize/saved_visualizations/_read_config'));
/**
* Transform an ES Response into data for this visualization
* @param {object} resp The elasticsearch response
* @return {array} An array of flattened response rows
*/
vis.buildChartDataFromResponse = Private(require('apps/visualize/saved_visualizations/_build_chart_data'));
}
return AdhocVis;
});
});

View file

@ -1,103 +0,0 @@
define(function (require) {
return function AggsService(Private) {
require('lodash');
var _ = require('lodash');
var aggs = {};
aggs.metricAggs = [
{
name: 'count',
display: 'Count',
types: ['number'],
makeLabel: function (params) {
return 'Count of documents';
}
},
{
name: 'avg',
display: 'Average',
types: ['number'],
makeLabel: function (params) {
return 'Average ' + params.field;
}
},
{
name: 'sum',
display: 'Sum',
types: ['number'],
makeLabel: function (params) {
return 'Sum of ' + params.field;
}
},
{
name: 'min',
display: 'Min',
types: ['number'],
makeLabel: function (params) {
return 'Min ' + params.field;
}
},
{
name: 'max',
display: 'Max',
types: ['number'],
makeLabel: function (params) {
return 'Max ' + params.field;
}
},
{
name: 'cardinality',
display: 'Unique count',
types: ['*'],
makeLabel: function (params) {
return 'Unique count of ' + params.field;
}
},
];
aggs.metricAggsByName = _.indexBy(aggs.metricAggs, 'name');
aggs.bucketAggs = Private(require('apps/visualize/saved_visualizations/bucket_aggs/_index'));
aggs.bucketAggsByName = _.indexBy(aggs.bucketAggs, 'name');
aggs.byName = _.assign({}, aggs.bucketAggsByName, aggs.metricAggsByName);
aggs.byFieldType = {
number: [
aggs.bucketAggsByName.terms,
aggs.bucketAggsByName.histogram,
aggs.bucketAggsByName.range,
// 'range'
],
date: [
// 'date range',
aggs.bucketAggsByName.date_histogram,
aggs.bucketAggsByName.terms,
],
boolean: [
aggs.bucketAggsByName.terms,
// 'terms'
],
ip: [
aggs.bucketAggsByName.terms,
aggs.bucketAggsByName.ip_range,
// 'ipv4 range'
],
geo_point: [
aggs.bucketAggsByName.terms,
// 'geo distance'
],
geo_shape: [
aggs.bucketAggsByName.terms,
// 'geohash grid'
],
string: [
// 'significant terms',
aggs.bucketAggsByName.terms,
// 'range'
]
};
return aggs;
};
});

View file

@ -1,53 +0,0 @@
define(function (require) {
var _ = require('lodash');
var categories = [
{
name: 'segment',
displayOrder: 2,
fetchOrder: 1,
min: 0,
max: Infinity,
configDefaults: {
size: 5
}
},
{
name: 'metric',
displayOrder: 1,
fetchOrder: 2,
min: 0,
max: 1,
configDefaults: {
agg: 'count'
}
},
{
name: 'group',
displayOrder: 3,
fetchOrder: 3,
min: 0,
max: 1,
configDefaults: {
global: false,
size: 5
}
},
{
name: 'split',
displayOrder: 4,
fetchOrder: 4,
min: 0,
max: 2,
configDefaults: {
size: 5,
row: true
}
}
];
categories.fetchOrder = _.sortBy(categories, 'fetchOrder');
categories.displayOrder = _.sortBy(categories, 'displayOrder');
categories.byName = _.indexBy(categories, 'name');
return categories;
});

View file

@ -1,111 +0,0 @@
define(function (require) {
return function ReadConfigFn(Private, $injector) {
var _ = require('lodash');
var configCategories = require('apps/visualize/saved_visualizations/_config_categories');
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
var courier = require('components/courier/courier');
return function readConfig() {
var vis = this;
// these arrays represent the different sections used to create an aggregation, and when config objects are encountered
// the are pushed into these array's based on their properties. Array's are used to make the logic and the final
// combination simple. Many of these will be limited to a single value by the UI
var positions = {
// used to create rows/columns
split: [],
// global segments (eg. color, marked in the ui to be applied gloabally and the same values should be used across all charts)
global: [],
// primary segments (eg. x-axis)
segment: [],
// local segments (eg. color, marked in the ui that it should apply within each chart)
local: [],
// metric is the root "measurement" (eg. y-axis)
metric: []
};
function moveValidatedParam(input, output, paramDef, name) {
if (!input[name]) {
if (paramDef.default != null) input[name] = _.cloneDeep(paramDef.default);
else return !paramDef.required;
}
var val = input[name];
var selectedOption = paramDef.options && _.find(paramDef.options, { val: val });
if (!paramDef.custom && paramDef.options && !selectedOption) return false;
if (paramDef.write) {
var selection = selectedOption;
// either the value is custom or there just aren't any options defined
if (!selectedOption && val != null) selection = { val: val };
// provide a hook to apply custom logic when writing this config value
paramDef.write(selection, output);
} else {
// copy over the param
output.aggParams[name] = val;
}
return true;
}
function makeCategoryValidator(category) {
return function categoryValidator(config) {
// filter out plain unusable configs
if (!config || !config.agg || !config.field) return;
// get the agg used by this config
var agg = aggs.byName[config.agg];
if (!agg || agg.name === 'count') return;
// copy parts of the config to the validated "output" object
var output = {
agg: config.agg,
aggParams: {},
categoryName: category.name
};
if (agg.name !== 'filters') output.aggParams.field = config.field;
// copy over other properties based on the category
switch (category.name) {
case 'split':
output.row = !!config.row;
break;
case 'group':
output.global = !!config.global;
break;
}
// this function will move valus from config.* to output.aggParams.* when they are
// needed for that aggregation, and return true or false based on if all requirements
// are meet
var moveToAggParams = _.partial(moveValidatedParam, config, output);
// ensure that all of the declared params for the agg are declared on the config
if (_.every(agg.params, moveToAggParams)) return output;
};
}
// collect all of the configs from each category,
// validate them, filter the invalid ones, and put them into positions
configCategories.fetchOrder.forEach(function (category) {
var configs = vis[category.name].configs;
configs = configs
.map(makeCategoryValidator(category))
.filter(Boolean);
if (category.name === 'group') {
positions.global = _.where(configs, { global: true });
positions.local = _.where(configs, { global: false });
} else {
positions[category.name] = configs;
}
});
// join all of the different positions into a single array
return positions.global.concat(positions.split, positions.segment, positions.local, positions.metric);
};
};
});

View file

@ -1,36 +1,28 @@
define(function (require) {
var _ = require('lodash');
var inherits = require('lodash').inherits;
require('modules')
.get('app/visualize')
.factory('SavedVis', function (config, $injector, courier, Promise, savedSearches, Private, Notifier) {
var _ = require('lodash');
var Vis = Private(require('components/vis/vis'));
var configCats = require('apps/visualize/saved_visualizations/_config_categories');
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
var module = require('modules').get('app/visualize');
module.factory('SavedVis', function (config, $injector, courier, indexPatterns, Promise, savedSearches, Private) {
var aggs = Private(require('apps/visualize/saved_visualizations/_aggs'));
var notify = new Notifier({
location: 'SavedVis'
});
_(SavedVis).inherits(courier.SavedObject);
function SavedVis(opts) {
var vis = this;
var self = this;
opts = opts || {};
if (typeof opts !== 'object') opts = { id: opts };
if (typeof opts !== 'object') {
opts = {
id: opts
};
}
var defaultParent = opts.parentSearchSource;
courier.SavedObject.call(vis, {
SavedVis.Super.call(self, {
type: 'visualization',
id: opts.id,
mapping: {
title: 'string',
typeName: 'string',
stateJSON: 'string',
visState: 'json',
description: 'string',
savedSearchId: 'string',
indexPattern: 'string'
@ -38,8 +30,12 @@ define(function (require) {
defaults: {
title: '',
typeName: opts.type || 'histogram',
stateJSON: null,
visState: (function () {
if (!opts.type) return null;
var def = {};
def.type = opts.type;
return def;
}()),
description: '',
savedSearchId: opts.savedSearchId,
indexPattern: opts.indexPattern
@ -47,173 +43,68 @@ define(function (require) {
searchSource: true,
afterESResp: function setVisState() {
if (!vis.typeDef || vis.typeName !== vis.typeDef.name) {
// refresh the typeDef
vis.typeDef = typeDefs.byName[vis.typeName];
// refresh the defaults for all config categories
configCats.forEach(function (category) {
vis._initConfigCategory(category, vis[category.name]);
});
}
afterESResp: this._afterEsResp
});
}
// get the saved state
var state;
if (vis.stateJSON) try { state = JSON.parse(vis.stateJSON); } catch (e) {}
SavedVis.prototype._afterEsResp = function () {
var self = this;
var relatedSearch = self.savedSearchId;
var relatedPattern = !relatedSearch && self.indexPattern;
// set the state on the vis
if (state) vis.setState(state);
var promisedParent = (function () {
if (relatedSearch) {
// returns a promise
return savedSearches.get(self.savedSearchId);
}
var relatedSearch = vis.savedSearchId;
var relatedPattern = !relatedSearch && vis.indexPattern;
var fakeSavedSearch = {
searchSource: courier.createSource('search')
};
var promisedParent = (function () {
if (relatedSearch) {
// returns a promise
return savedSearches.get(vis.savedSearchId);
}
var fakeSavedSearch = {
searchSource: courier.createSource('search')
};
if (relatedPattern) {
return indexPatterns.get(relatedPattern)
.then(function (indexPattern) {
fakeSavedSearch.searchSource.index(indexPattern);
return fakeSavedSearch;
});
}
return Promise.resolve(fakeSavedSearch);
}());
return promisedParent
.then(function (parent) {
vis.savedSearch = parent;
vis.searchSource
.inherits(parent.searchSource)
.size(0)
// reads the vis' config and write the agg to the searchSource
.aggs(function () {
// stores the config objects in queryDsl
var dsl = {};
// counter to ensure unique agg names
var i = 0;
// start at the root, but the current will move
var current = dsl;
// continue to nest the aggs under each other
// writes to the dsl object
vis.getConfig().forEach(function (config) {
current.aggs = {};
var key = '_agg_' + (i++);
var aggDsl = {};
aggDsl[config.agg] = config.aggParams;
current = current.aggs[key] = aggDsl;
});
// set the dsl to the searchSource
return dsl.aggs || {};
});
vis._fillConfigsToMinimum();
_.assign(vis, vis.typeDef.listeners);
return vis;
if (relatedPattern) {
return courier.indexPatterns.get(relatedPattern)
.then(function (indexPattern) {
fakeSavedSearch.searchSource.index(indexPattern);
return fakeSavedSearch;
});
}
return Promise.resolve(fakeSavedSearch);
}());
return promisedParent
.then(function (parent) {
self.savedSearch = parent;
self.searchSource
.inherits(parent.searchSource)
.size(0);
if (!self.vis) {
self.vis = self._createVis();
} else {
self.vis.indexPattern = self.searchSource.get('index');
self.vis.setState(self.visState);
}
self.searchSource.aggs(function () {
return self.vis.aggs.toDSL();
});
return self;
});
};
vis.addConfig = function (categoryName) {
var category = configCats.byName[categoryName];
var config = _.defaults({}, category.configDefaults);
SavedVis.prototype._createVis = function () {
var indexPattern = this.searchSource.get('index');
vis[category.name].configs.push(config);
if (this.stateJSON) {
this.visState = Vis.convertOldState(this.typeName, JSON.parse(this.stateJSON));
}
return config;
};
vis.removeConfig = function (config) {
if (!config) return;
configCats.forEach(function (category) {
_.pull(vis[category.name].configs, config);
});
};
vis._fillConfigsToMinimum = function () {
// satify the min count for each category
configCats.fetchOrder.forEach(function (category) {
var myCat = vis[category.name];
if (myCat.configs.length < myCat.min) {
_.times(myCat.min - myCat.configs.length, function () {
vis.addConfig(category.name);
});
}
});
};
// init the config category, optionally pass in an existing category to refresh
// it's defaults based on the
vis._initConfigCategory = function (category, cat) {
cat = cat || {};
if (vis.typeDef) _.assign(cat, category, vis.typeDef.config[category.name]);
cat.configDefaults = _.clone(category.configDefaults),
cat.configs = cat.config || [];
vis[category.name] = cat;
return cat;
};
vis.setState = function (state) {
configCats.forEach(function (category) {
var categoryStates = state[category.name] || [];
vis[category.name].configs.splice(0);
categoryStates.forEach(function (configState) {
var config = vis.addConfig(category.name);
_.assign(config, configState);
});
});
vis._fillConfigsToMinimum();
};
vis.getState = function () {
return _.transform(configCats, function (state, category) {
var configs = state[category.name] = [];
[].push.apply(configs, vis[category.name].configs.map(function (config) {
return _.pick(config, function (val, key) {
return key.substring(0, 2) !== '$$';
});
}));
}, {});
};
/**
* Create a list of config objects, which are ready to be turned into aggregations,
* in the order which they should be executed.
*
* @return {Array} - The list of config objects
*/
vis.getConfig = Private(require('apps/visualize/saved_visualizations/_read_config'));
/**
* Transform an ES Response into data for this visualization
* @param {object} resp The elasticsearch response
* @return {array} An array of flattened response rows
*/
vis.buildChartDataFromResponse = Private(require('apps/visualize/saved_visualizations/_build_chart_data'));
}
inherits(SavedVis, courier.SavedObject);
return new Vis(indexPattern, this.visState);
};
return SavedVis;
});

View file

@ -1,153 +0,0 @@
define(function (require) {
var module = require('modules').get('apps/visualize');
var _ = require('lodash');
var typeDefs = [
{
name: 'histogram',
icon: 'icon-chart-bar',
params: {
shareYAxis: true,
addTooltip: true,
addLegend: true
},
listeners: {
onClick: function (e) {
// TODO: We need to be able to get ahold of angular services here
console.log(e);
}
},
config: {
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
segment: {
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
min: 0,
max: 1
},
split: {
label: 'Rows & Columns',
min: 0,
max: 1
}
}
},
{
name: 'line',
icon: 'icon-chart-bar',
params: {
shareYAxis: true,
addTooltip: true,
addLegend: true
},
listeners: {
},
config: {
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
segment: {
// limitToOrderedAggs: true,
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
min: 0,
max: 1
},
split: {
label: 'Rows & Columns',
min: 0,
max: 1
}
}
},
{
name: 'area',
icon: 'icon-chart-bar',
params: {
shareYAxis: true,
addTooltip: true,
addLegend: true,
isStacked: true
},
listeners: {
onClick: function (e) {
// TODO: We need to be able to get ahold of angular services here
console.log(e);
}
},
config: {
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
segment: {
// limitToOrderedAggs: true,
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
min: 0,
max: 1
},
split: {
label: 'Rows & Columns',
min: 0,
max: 1
}
}
},
{
name: 'pie',
icon: 'icon-chart-bar',
params: {
addTooltip: true,
addLegend: true
},
listeners: {
},
config: {
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
segment: {
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
min: 0,
max: 1
},
split: {
label: 'Rows & Columns',
min: 0,
max: 1
}
}
}
];
typeDefs.byName = _.indexBy(typeDefs, 'name');
return typeDefs;
});

View file

@ -1,13 +0,0 @@
define(function (require) {
return function AggsService(Private) {
return [
Private(require('apps/visualize/saved_visualizations/bucket_aggs/date_histogram')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/histogram')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/range')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/ip_range')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/terms')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/filters')),
Private(require('apps/visualize/saved_visualizations/bucket_aggs/significant_terms'))
];
};
});

View file

@ -1,119 +0,0 @@
define(function (require) {
return function DateHistogramAggDefinition(timefilter, config) {
var _ = require('lodash');
var moment = require('moment');
var interval = require('utils/interval');
// shorthand
var ms = function (type) { return moment.duration(1, type).asMilliseconds(); };
var pickInterval = function (bounds, targetBuckets) {
bounds || (bounds = timefilter.getBounds());
return interval.calculate(bounds.min, bounds.max, targetBuckets);
};
var agg = this;
agg.name = 'date_histogram';
agg.display = 'Date Histogram';
agg.ordered = {date: true};
agg.makeLabel = function (params, fullConfig) {
if (fullConfig.metricScaleText) return params.field + ' per ' + fullConfig.metricScaleText;
var aggInterval = _.find(agg.params.interval.options, { ms: interval.toMs(params.interval) });
if (aggInterval) return aggInterval.display + ' ' + params.field;
else return params.field + ' per ' + interval.describe(params.interval);
};
agg.params = {};
agg.params.interval = {
required: true,
default: 'auto',
custom: true,
options: [
{
display: 'Auto',
val: 'auto'
},
{
display: 'Second',
val: 'second',
ms: ms('second')
},
{
display: 'Minute',
val: 'minute',
ms: ms('minute')
},
{
display: 'Hourly',
val: 'hour',
ms: ms('hour')
},
{
display: 'Daily',
val: 'day',
ms: ms('day')
},
{
display: 'Weekly',
val: 'week',
ms: ms('week')
},
{
display: 'Monthly',
val: 'month',
ms: ms('month')
},
{
display: 'Yearly',
val: 'year',
ms: ms('year')
}
],
write: function (selection, output) {
var bounds = timefilter.getBounds();
var auto;
if (selection.val === 'auto') {
var bucketTarget = config.get('histogram:barTarget');
auto = pickInterval(bounds, bucketTarget);
output.aggParams.interval = auto.interval + 'ms';
output.metricScaleText = auto.description;
return;
}
var ms = selection.ms || interval.toMs(selection.val);
var buckets = Math.ceil((bounds.max - bounds.min) / ms);
var maxBuckets = config.get('histogram:maxBars');
if (buckets > maxBuckets) {
// we should round these buckets out, and scale back the y values
auto = pickInterval(bounds, maxBuckets);
output.aggParams.interval = auto.interval + 'ms';
output.metricScale = ms / auto.interval;
output.metricScaleText = selection.val || auto.description;
} else {
output.aggParams.interval = selection.val;
}
}
};
agg.params.format = {
hide: true,
custom: true
};
agg.params.extended_bounds = {
hide: true,
default: {},
write: function (selection, output) {
var bounds = timefilter.getBounds();
output.aggParams.extended_bounds = {
min: bounds.min,
max: bounds.max
};
}
};
};
});

View file

@ -1,38 +0,0 @@
define(function (require) {
return function FiltersAggDefinition(timefilter, config) {
var _ = require('lodash');
var moment = require('moment');
var angular = require('angular');
var agg = this;
agg.name = 'filters';
agg.display = 'Filters';
agg.makeLabel = function (params) {
return 'Filters';
};
function getTickLabel(query) {
if (query.query_string && query.query_string.query) return query.query_string.query;
return JSON.stringify(query);
}
agg.params = {};
agg.params.filters = {
custom: true,
default: {query_string: {query: '*'}},
write: function (input, output) {
output.aggParams = {
filters: _.zipObject(_.map(input.val, function (filter, iterator) {
// We need to check here
return [
getTickLabel(filter.input),
{query: filter.input || {query_string: {query: '*'}}}
];
}))
};
}
};
};
});

View file

@ -1,44 +0,0 @@
define(function (require) {
return function HistogramAggDefinition(timefilter, config) {
var _ = require('lodash');
var moment = require('moment');
var agg = this;
agg.name = 'histogram';
agg.display = 'Histogram';
agg.ordered = {};
agg.makeLabel = function (params) {
return params.field;
};
agg.params = {};
agg.params.interval = {
required: true,
write: function (input, output) {
output.aggParams.interval = parseInt(input.val, 10);
}
};
agg.params.min_doc_count = {
custom: true,
default: false,
write: function (input, output) {
if (input.val) output.aggParams.min_doc_count = 0;
else delete output.aggParams.min_doc_count;
}
};
agg.params.extended_bounds = {
default: {},
write: function (input, output) {
output.aggParams.extended_bounds = {
min: input.val.min,
max: input.val.max
};
}
};
};
});

View file

@ -1,31 +0,0 @@
define(function (require) {
require('directives/validate_ip');
return function RangeAggDefinition(timefilter, config) {
var _ = require('lodash');
var moment = require('moment');
var angular = require('angular');
var agg = this;
agg.name = 'ip_range';
agg.display = 'IP Range';
//agg.ordered = {};
agg.makeLabel = function (params) {
return params.field;
};
agg.params = {};
agg.params.ranges = {
custom: true,
default: [{from: '0.0.0.0', to: '255.255.255.255'}],
write: function (input, output) {
output.aggParams.ranges = input.val;
output.aggParams.keyed = true;
}
};
};
});

View file

@ -1,29 +0,0 @@
define(function (require) {
return function RangeAggDefinition(timefilter, config) {
var _ = require('lodash');
var moment = require('moment');
var angular = require('angular');
var agg = this;
agg.name = 'range';
agg.display = 'Range';
//agg.ordered = {};
agg.makeLabel = function (params) {
return params.field;
};
agg.params = {};
agg.params.ranges = {
custom: true,
default: [{from: 0, to: 1000}, {from: 1000, to: 2000}],
write: function (input, output) {
output.aggParams.ranges = input.val;
output.aggParams.keyed = true;
}
};
};
});

View file

@ -1,19 +0,0 @@
define(function (require) {
return function SignificantTermsAggDefinition() {
var _ = require('lodash');
var agg = this;
agg.name = 'significant_terms';
agg.display = 'Significant Terms';
agg.makeLabel = function (params) {
return 'Top ' + params.size + ' unusual terms in ' + params.field;
};
agg.params = {
size: {
required: false,
}
};
};
});

View file

@ -1,32 +0,0 @@
define(function (require) {
return function TermsAggDefinition() {
var _ = require('lodash');
var agg = this;
agg.name = 'terms';
agg.display = 'Terms';
agg.makeLabel = function (params) {
var order = _.find(agg.params.order.options, { val: params.order._count });
return order.display + ' ' + params.size + ' ' + params.field;
};
agg.params = {
size: {
required: false,
},
order: {
required: true,
options: [
{ display: 'Top', val: 'desc' },
{ display: 'Bottom', val: 'asc' }
],
default: 'desc',
write: function (selection, output) {
// TODO: We need more just _count here.
output.aggParams.order = { _count: selection.val };
}
}
};
};
});

View file

@ -1,11 +0,0 @@
define(function (require) {
return function RespConvertersService(Private) {
var histogram = Private(require('apps/visualize/saved_visualizations/resp_converters/histogram'));
return {
histogram: histogram,
line: histogram,
area: histogram,
pie: histogram
};
};
});

View file

@ -1,6 +1,5 @@
define(function (require) {
var app = require('modules').get('apps/visualize');
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
var app = require('modules').get('app/visualize');
var _ = require('lodash');
require('apps/visualize/saved_visualizations/_saved_vis');
@ -12,7 +11,11 @@ define(function (require) {
title: 'visualizations'
});
app.service('savedVisualizations', function (Promise, es, config, SavedVis) {
app.service('savedVisualizations', function (Promise, es, config, SavedVis, Private, Notifier) {
var visTypes = Private(require('components/vis_types/index'));
var notify = new Notifier({
location: 'saved visualization service'
});
this.get = function (id) {
return (new SavedVis(id)).init();
@ -49,14 +52,26 @@ define(function (require) {
.then(function (resp) {
return {
total: resp.hits.total,
hits: resp.hits.hits.map(function (hit) {
hits: _.transform(resp.hits.hits, function (hits, hit) {
var source = hit._source;
source.id = hit._id;
source.url = self.urlFor(hit._id);
source.typeDef = typeDefs.byName[source.typeName];
source.icon = source.typeDef.icon;
return source;
})
var typeName = source.typeName;
if (source.visState) {
try { typeName = JSON.parse(source.visState).type; }
catch (e) { /* missing typename handled below */ }
}
if (!typeName) {
notify.info('unable to detect type from visualization source', hit);
return;
}
source.type = visTypes.byName[typeName];
source.icon = source.type.icon;
hits.push(source);
}, [])
};
});
};

View file

@ -1,56 +0,0 @@
define(function (require) {
return function VisSpyReqRespStats() {
var reqRespStatsHTML = require('text!apps/visualize/spy/_req_resp_stats.html');
var linkReqRespStats = function ($scope, config) {
$scope.$watchCollection('vis.searchSource.history', function (searchHistory) {
if (!searchHistory) {
$scope.history = [];
return;
}
$scope.history = searchHistory.map(function (entry) {
if (!entry.complete || !entry.state) return;
var state = entry.state;
var resp = entry.resp;
var meta = [];
if (resp && resp.took != null) meta.push(['Query Duration', resp.took + 'ms']);
if (entry && entry.ms != null) meta.push(['Request Duration', entry.ms + 'ms']);
if (resp && resp.hits) meta.push(['Hits', resp.hits.total]);
if (state.index) meta.push(['Index', state.index]);
if (state.type) meta.push(['Type', state.type]);
if (state.id) meta.push(['Id', state.id]);
return {
meta: meta,
req: state.body,
resp: entry.resp
};
}).filter(Boolean);
});
};
return [
{
name: 'request',
display: 'Request',
template: reqRespStatsHTML,
link: linkReqRespStats
},
{
name: 'response',
display: 'Response',
template: reqRespStatsHTML,
link: linkReqRespStats
},
{
name: 'stats',
display: 'Statistics',
template: reqRespStatsHTML,
link: linkReqRespStats
}
];
};
});

View file

@ -1,103 +1,13 @@
@import (reference) "../../../styles/_mixins.less";
@import (reference) "../../../styles/_bootstrap.less";
@import (reference) "../../../styles/theme/_theme.less";
@import (reference) "../../../styles/_variables.less";
@import (reference) "lesshat.less";
@media (min-width: @screen-md-min) {
.vis-editor-content {
display: flex;
.flex-direction(row);
.justify-content(flex-start);
.vis-sidebar {
.flex(0, 0, 300px);
}
.vis-canvas {
.flex(1, 1, 100%);
}
}
}
.vis-editor-content {
vis-config-editor {
display: block;
}
.sidebar-item-title:hover {
color: inherit !important;
background-color: inherit !important;
}
.vis-config-details {
border-top: 1px solid @well-border;
padding: 5px 10px;
background-color: @body-bg;
color: @text-color;
.config-controls {
margin-bottom: 5px;
}
}
.agg-config-interval {
td {
padding-left: 10px;
&:first-child {
padding-left: 0px;
}
}
}
}
.vis-wizard {
h1 {
margin-top: 45px;
}
}
vis-canvas {
display: block;
}
.vis-editor navbar {
.bitty-modal-container {
position: relative;
.bitty-modal {
position: absolute;
cursor: pointer;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 10;
background: rgba(70, 82, 93, 0.9);
color: white;
text-align: center;
padding-top: 6px;
.user-select(none);
}
}
}
form.vis-share {
div.form-control {
height: inherit;
}
}
// vis-search-editor {
// display: block;
// background: @sidebar-bg;
// text-align: center;
// min-height: 0;
// border-bottom: 1px solid darken(@sidebar-bg, 10%);
// .user-select(none);
// color: @sidebar-color;
// a {
// color: @sidebar-color;
// }
// }
@import "../editor/editor.less";

View file

@ -5,8 +5,8 @@
<ul class="list-group list-group-menu">
<a class="list-group-item list-group-menu-item"
ng-repeat="type in visTypeDefs"
ng-href="{{ typeUrl(type) }}">
ng-repeat="type in visTypes"
ng-href="{{ visTypeUrl(type) }}">
<li>
<i ng-class="type.icon"></i>{{type.name}}
</li>

View file

@ -1,26 +1,22 @@
define(function (require) {
var _ = require('lodash');
var typeDefs = require('apps/visualize/saved_visualizations/_type_defs');
require('apps/visualize/saved_visualizations/saved_visualizations');
require('directives/saved_object_finder');
require('apps/discover/saved_searches/saved_searches');
var app = require('modules').get('apps/visualize', [
'kibana/courier'
]);
var routes = require('routes');
var templateStep = function (num, txt) {
return '<div ng-controller="VisualizeWizardStep' + num + '" class="container vis-wizard">' + txt + '</div>';
};
var module = require('modules').get('app/visualize', ['kibana/courier']);
var routes = require('routes');
/********
/** Wizard Step 1
/********/
routes.when('/visualize/step/1', {
template: templateStep(1, require('text!apps/visualize/partials/wizard/step_1.html')),
template: templateStep(1, require('text!apps/visualize/wizard/step_1.html')),
resolve: {
indexPatternIds: function (courier) {
return courier.indexPatterns.getIds();
@ -28,12 +24,12 @@ define(function (require) {
}
});
app.controller('VisualizeWizardStep1', function ($route, $scope, $location, timefilter) {
module.controller('VisualizeWizardStep1', function ($route, $scope, $location, timefilter) {
$scope.step2WithSearchUrl = function (hit) {
return '#/visualize/step/2?savedSearchId=' + encodeURIComponent(hit.id);
};
timefilter.enabled(false);
timefilter.enabled = false;
$scope.indexPattern = {
selection: null,
@ -50,18 +46,18 @@ define(function (require) {
/** Wizard Step 2
/********/
routes.when('/visualize/step/2', {
template: templateStep(2, require('text!apps/visualize/partials/wizard/step_2.html'))
template: templateStep(2, require('text!apps/visualize/wizard/step_2.html'))
});
app.controller('VisualizeWizardStep2', function ($scope, $route, $location, timefilter) {
module.controller('VisualizeWizardStep2', function ($scope, $route, $location, timefilter, Private) {
var existing = _.pick($route.current.params, 'indexPattern', 'savedSearchId');
timefilter.enabled(false);
timefilter.enabled = false;
$scope.visTypeDefs = typeDefs;
$scope.typeUrl = function (type) {
$scope.visTypes = Private(require('components/vis_types/index'));
$scope.visTypeUrl = function (visType) {
var query = _.defaults({
type: type.name
type: visType.name
}, existing);
return '#/visualize/create?' + _.map(query, function (val, key) {

View file

@ -0,0 +1,18 @@
| Private | module id |
| --- | --- |
| `true` | `components/agg_config/index` |
```js
var aggTypes = Private(require('components/agg_types/index'));
```
Collection of `AggType` definition objects. See the [Vis component](../vis) for an overall explaination of how `AggTypes` are used.
### Included
- [`AggType`](_agg_type.js) class
- `AggParam` classes
- [`BaseAggParam`](param_types/base.js)
- [`FieldAggParam`](param_types/field.js)
- [`OptionedAggParam`](param_types/optioned.js)
- [`AggParams`](_agg_params.js) class

View file

@ -0,0 +1,46 @@
define(function (require) {
return function AggParamsFactory(Private) {
var _ = require('lodash');
var Registry = require('utils/registry/registry');
var BaseAggParam = Private(require('components/agg_types/param_types/base'));
var FieldAggParam = Private(require('components/agg_types/param_types/field'));
var OptionedAggParam = Private(require('components/agg_types/param_types/optioned'));
_(AggParams).inherits(Registry);
function AggParams(params) {
AggParams.Super.call(this, {
index: ['name'],
group: ['required'],
initialSet: params.map(function (param) {
if (param.name === 'field') {
return new FieldAggParam(param);
}
else if (param.options) {
return new OptionedAggParam(param);
}
else {
return new BaseAggParam(param);
}
})
});
}
AggParams.prototype.write = function (aggConfig, locals) {
var output = { params: {} };
locals = locals || {};
this.forEach(function (param) {
if (param.write) {
param.write(aggConfig, output, locals);
} else {
output.params[param.name] = aggConfig.params[param.name];
}
});
return output;
};
return AggParams;
};
});

View file

@ -0,0 +1,29 @@
define(function (require) {
return function AggTypeFactory(Private) {
var _ = require('lodash');
var AggParams = Private(require('components/agg_types/_agg_params'));
function AggType(config) {
this.name = config.name;
this.title = config.title;
this.makeLabel = config.makeLabel || _.constant(this.name);
this.ordered = config.ordered;
var params = this.params = config.params || [];
if (!(params instanceof AggParams)) {
if (_.isPlainObject(params)) {
// convert the names: details format into details[].name
params = _.map(params, function (param, name) {
param.name = name;
return param;
});
}
params = this.params = new AggParams(params);
}
}
return AggType;
};
});

View file

@ -0,0 +1,50 @@
define(function (require) {
return function IntervalOptionsService(Private) {
var moment = require('moment');
// shorthand
var ms = function (type) { return moment.duration(1, type).asMilliseconds(); };
return [
{
display: 'Auto',
val: 'auto'
},
{
display: 'Second',
val: 'second',
ms: ms('second')
},
{
display: 'Minute',
val: 'minute',
ms: ms('minute')
},
{
display: 'Hourly',
val: 'hour',
ms: ms('hour')
},
{
display: 'Daily',
val: 'day',
ms: ms('day')
},
{
display: 'Weekly',
val: 'week',
ms: ms('week')
},
{
display: 'Monthly',
val: 'month',
ms: ms('month')
},
{
display: 'Yearly',
val: 'year',
ms: ms('year')
}
];
};
});

View file

@ -0,0 +1,117 @@
define(function (require) {
return function DateHistogramAggType(timefilter, config, Private) {
var _ = require('lodash');
var moment = require('moment');
var interval = require('utils/interval');
var AggType = Private(require('components/agg_types/_agg_type'));
require('filters/field_type');
var pickInterval = function (bounds, targetBuckets) {
bounds || (bounds = timefilter.getBounds());
return interval.calculate(bounds.min, bounds.max, targetBuckets);
};
return new AggType({
name: 'date_histogram',
title: 'Date Histogram',
ordered: {
date: true
},
makeLabel: function (aggConfig) {
var output = this.params.write(aggConfig);
var params = output.params;
if (output.metricScaleText) return params.field + ' per ' + output.metricScaleText;
var aggInterval = _.find(this.params.byName.interval.options, {
ms: interval.toMs(params.interval)
});
if (aggInterval) return aggInterval.display + ' ' + params.field;
else return params.field + ' per ' + interval.describe(params.interval);
},
params: [
{
name: 'field',
filterFieldTypes: 'date'
},
{
name: 'interval',
default: 'auto',
options: Private(require('components/agg_types/buckets/_interval_options')),
editor: require('text!components/agg_types/controls/interval.html'),
write: function (aggConfig, output, locals) {
var bounds = timefilter.getBounds();
var auto;
var selection = aggConfig.params.interval;
if (!_.isObject(selection)) {
// custom selection
selection = {
display: selection,
val: selection
};
}
if (selection.val === 'auto') {
if (locals.renderBot) {
throw new Error('not implemented');
}
var bucketTarget = config.get('histogram:barTarget');
auto = pickInterval(bounds, bucketTarget);
output.params.interval = auto.interval + 'ms';
output.metricScaleText = auto.description;
return;
}
var ms = selection.ms || interval.toMs(selection.val);
var buckets = Math.ceil((bounds.max - bounds.min) / ms);
var maxBuckets = config.get('histogram:maxBars');
if (buckets > maxBuckets) {
// we should round these buckets out, and scale back the y values
auto = pickInterval(bounds, maxBuckets);
output.params.interval = auto.interval + 'ms';
output.metricScale = ms / auto.interval;
output.metricScaleText = selection.val || auto.description;
} else {
output.params.interval = selection.val;
}
}
},
{
name: 'format'
},
{
name: 'min_doc_count',
default: 1
},
{
name: 'extended_bounds',
default: {},
write: function (aggConfig, output) {
var val = aggConfig.params.extended_bounds;
if (val.min != null || val.max != null) {
output.params.extended_bounds = {
min: val.min,
max: val.max
};
} else {
var tfBounds = timefilter.getBounds();
output.params.extended_bounds = {
min: tfBounds.min,
max: tfBounds.max,
};
}
}
}
]
});
};
});

View file

@ -0,0 +1,36 @@
define(function (require) {
return function FiltersAggDefinition(Private) {
var _ = require('lodash');
var AggType = Private(require('components/agg_types/_agg_type'));
function getTickLabel(query) {
if (query.query_string && query.query_string.query) {
return query.query_string.query;
}
return JSON.stringify(query);
}
return new AggType({
name: 'filters',
title: 'Filters',
params: [
{
name: 'filters',
editor: require('text!components/agg_types/controls/filters.html'),
default: [ {} ],
write: function (aggConfig, output) {
output.aggParams = {
filters: _.transform(aggConfig.params.filters, function (filters, filter, iterator) {
// We need to check here
filters[getTickLabel(filter.input)] = {
query: filter.input || { query_string: {query: '*'} }
};
}, {})
};
}
}
]
});
};
});

View file

@ -0,0 +1,68 @@
define(function (require) {
return function HistogramAggDefinition(Private) {
var _ = require('lodash');
var moment = require('moment');
var AggType = Private(require('components/agg_types/_agg_type'));
return new AggType({
name: 'histogram',
title: 'Histogram',
ordered: {},
makeLabel: function (aggConfig) {
return aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
},
{
name: 'interval',
editor: require('text!components/agg_types/controls/interval.html'),
write: function (aggConfig, output) {
output.params.interval = parseInt(aggConfig.params.interval, 10);
}
},
{
name: 'min_doc_count',
default: false,
editor: require('text!components/agg_types/controls/min_doc_count.html'),
write: function (aggConfig, output) {
if (aggConfig.params.min_doc_count) {
output.params.min_doc_count = 0;
}
}
},
{
name: 'extended_bounds',
default: {},
editor: require('text!components/agg_types/controls/extended_bounds.html'),
write: function (aggConfig, output) {
var val = aggConfig.params.extended_bounds;
if (val.min != null || val.max != null) {
output.params.extended_bounds = {
min: val.min,
max: val.max
};
}
},
// called from the editor
shouldShow: function (aggConfig) {
var field = aggConfig.params.field;
if (
field
&& (field.type === 'number' || field.type === 'date')
) {
return true;
}
}
}
]
});
};
});

View file

@ -0,0 +1,34 @@
define(function (require) {
return function RangeAggDefinition(Private) {
var _ = require('lodash');
var moment = require('moment');
var angular = require('angular');
var AggType = Private(require('components/agg_types/_agg_type'));
return new AggType({
name: 'range',
title: 'Range',
makeLabel: function (aggConfig) {
return aggConfig.params.field.name + ' ranges';
},
params: [
{
name: 'field',
filterFieldTypes: ['number', 'date', 'string']
},
{
name: 'ranges',
default: [
{ from: 0, to: 1000 },
{ from: 1000, to: 2000 }
],
editor: require('text!components/agg_types/controls/ranges.html'),
write: function (aggConfig, output) {
output.params.ranges = aggConfig.params.ranges;
output.params.keyed = true;
}
}
]
});
};
});

View file

@ -0,0 +1,24 @@
define(function (require) {
return function SignificantTermsAggDefinition(Private) {
var _ = require('lodash');
var AggType = Private(require('components/agg_types/_agg_type'));
return new AggType({
name: 'significant_terms',
title: 'Significant Terms',
makeLabel: function (aggConfig) {
return 'Top ' + aggConfig.params.size + ' unusual terms in ' + aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'string'
},
{
name: 'size',
editor: require('text!components/agg_types/controls/order_and_size.html'),
}
]
});
};
});

View file

@ -0,0 +1,40 @@
define(function (require) {
return function TermsAggDefinition(Private) {
var _ = require('lodash');
var AggType = Private(require('components/agg_types/_agg_type'));
return new AggType({
name: 'terms',
title: 'Terms',
makeLabel: function (aggConfig) {
var params = aggConfig.params;
return params.order.display + ' ' + params.size + ' ' + params.field.name;
},
params: [
{
name: 'field'
},
{
name: 'size',
default: 5
// editor: batched with order
},
{
name: 'order',
options: [
{ display: 'Top', val: 'desc' },
{ display: 'Bottom', val: 'asc' }
],
editor: require('text!components/agg_types/controls/order_and_size.html'),
default: 'desc',
write: function (aggConfig, output) {
// TODO: We need more than just _count here.
output.params.order = {
_count: aggConfig.params.order.val
};
}
}
]
});
};
});

View file

@ -0,0 +1,18 @@
<div ng-if="aggParam.shouldShow(aggConfig)" class="vis-editor-agg-form-row">
<div class="form-group">
<label>Min <small>(optional)</small></label>
<input
ng-model="params.extended_bounds.min"
type="number"
class="form-control"
name="extended_bounds.min" />
</div>
<div class="form-group">
<label>Max <small>(optional)</small></label>
<input
ng-model="params.extended_bounds.max"
type="number"
class="form-control"
name="extended_bounds.max" />
</div>
</div>

View file

@ -0,0 +1,17 @@
<div class="form-group">
<label for="field">
Field
</label>
<select
class="form-control"
name="field"
required
ng-model="params.field"
ng-options="
field as field.name group by field.type for field in aggConfig.vis.indexPattern.fields.raw
| fieldType: aggParam.filterFieldTypes
| filter: { indexed:true }
| orderBy:['type','name']
">
</select>
</div>

View file

@ -0,0 +1,24 @@
<div class="form-group">
<div ng-repeat="filter in params.filters">
<label>Query string {{$index + 1}}</label>
<div class="form-group vis-editor-agg-form-row">
<input query-input
ng-model="filter.query"
type="text"
class="form-control"
name="filter{{$index}}">
<button
type="button"
ng-click="params.filters.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<div
ng-click="params.filters.push({})"
class="sidebar-item-button primary">
Add filter
</div>

View file

@ -0,0 +1,18 @@
<div class="form-group">
<label>Interval</label>
<select
ng-if="aggParam.options"
ng-model="params.interval"
required
ng-options="opt as opt.display for opt in aggParam.options"
class="form-control"
name="interval">
</select>
<input
ng-if="!aggParam.options"
ng-model="params.interval"
required
type="number"
class="form-control"
name="interval" />
</div>

View file

@ -0,0 +1,10 @@
<div class="checkbox ng-scope">
<label>
<input ng-model="params.min_doc_count" type="checkbox">
Show empty buckets&nbsp;
<kbn-info
info="Show all buckets, not only the buckets with results."
placement="right">
</kbn-info>
</label>
</div>

View file

@ -0,0 +1,21 @@
<div class="vis-editor-agg-form-row">
<div ng-if="aggType.params.byName.order" class="form-group">
<label>Order</label>
<select
name="order"
ng-model="params.order"
required
ng-options="opt as opt.display for opt in aggParam.options"
class="form-control">
</select>
</div>
<div class="form-group">
<label>Size</label>
<input
name="size"
ng-model="params.size"
required
class="form-control"
type="number">
</div>
</div>

View file

@ -0,0 +1,39 @@
<table class="vis-editor-agg-editor-ranges form-group">
<tr>
<th>
<label>From</label>
</th>
<th colspan="2">
<label>To</label>
</th>
</tr>
<tr
ng-repeat="range in params.ranges track by $index">
<td>
<input
ng-model="range.from"
type="number"
class="form-control"
name="range.from" />
</td>
<td>
<input
ng-model="range.to"
type="number"
class="form-control"
name="range.to" />
</td>
<td>
<button ng-click="params.ranges.splice($index, 1)"
class="btn btn-danger btn-xs">
<i class="fa fa-ban" ></i>
</button>
</td>
</tr>
</table>
<div
ng-click="params.ranges.push({})"
class="sidebar-item-button primary">
Add Range
</div>

View file

@ -0,0 +1,29 @@
define(function (require) {
return function AggTypeService(Private) {
var Registry = require('utils/registry/registry');
var aggs = {
metrics: Private(require('components/agg_types/metric_aggs')),
buckets: [
Private(require('components/agg_types/buckets/date_histogram')),
Private(require('components/agg_types/buckets/histogram')),
Private(require('components/agg_types/buckets/range')),
Private(require('components/agg_types/buckets/terms')),
Private(require('components/agg_types/buckets/filters')),
Private(require('components/agg_types/buckets/significant_terms'))
]
};
Object.keys(aggs).forEach(function (type) {
aggs[type].forEach(function (agg) {
agg.type = type;
});
});
return new Registry({
index: ['name'],
group: ['type'],
initialSet: aggs.metrics.concat(aggs.buckets)
});
};
});

View file

@ -0,0 +1,81 @@
define(function (require) {
return function MetricAggsService(Private) {
var AggType = Private(require('components/agg_types/_agg_type'));
return [
{
name: 'count',
title: 'Count',
makeLabel: function (aggConfig) {
return 'Count of documents';
}
},
{
name: 'avg',
title: 'Average',
makeLabel: function (aggConfig) {
return 'Average ' + aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
}
]
},
{
name: 'sum',
title: 'Sum',
makeLabel: function (aggConfig) {
return 'Sum of ' + aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
}
]
},
{
name: 'min',
title: 'Min',
makeLabel: function (aggConfig) {
return 'Min ' + aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
}
]
},
{
name: 'max',
title: 'Max',
makeLabel: function (aggConfig) {
return 'Max ' + aggConfig.params.field.name;
},
params: [
{
name: 'field',
filterFieldTypes: 'number'
}
]
},
{
name: 'cardinality',
title: 'Unique count',
makeLabel: function (aggConfig) {
return 'Unique count of ' + aggConfig.params.field.name;
},
params: [
{
name: 'field'
}
]
}
].map(function (def) {
return new AggType(def);
});
};
});

View file

@ -0,0 +1,11 @@
define(function (require) {
return function BaseAggParamFactory() {
var _ = require('lodash');
function BaseAggParam(config) {
_.assign(this, config);
}
return BaseAggParam;
};
});

View file

@ -0,0 +1,54 @@
define(function (require) {
return function FieldAggParamFactory(Private) {
var _ = require('lodash');
var editorHtml = require('text!components/agg_types/controls/field.html');
var BaseAggParam = Private(require('components/agg_types/param_types/base'));
_(FieldAggParam).inherits(BaseAggParam);
function FieldAggParam(config) {
FieldAggParam.Super.call(this, config);
}
FieldAggParam.prototype.editor = editorHtml;
FieldAggParam.prototype.filterFieldTypes = '*';
/**
* Called to serialize values for saving an aggConfig object
*
* @param {field} field - the field that was selected
* @return {string}
*/
FieldAggParam.prototype.serialize = function (field) {
return field.name;
};
/**
* Called to read values from a database record into the
* aggConfig object
*
* @param {string} fieldName
* @return {field}
*/
FieldAggParam.prototype.deserialize = function (fieldName, aggConfig) {
return aggConfig.vis.indexPattern.fields.byName[fieldName];
};
/**
* Write the aggregation parameter.
*
* @param {AggConfig} aggConfig - the entire configuration for this agg
* @param {object} output - the result of calling write on all of the aggregations
* parameters.
* @param {object} output.param - the final object that will be included as the params
* for the agg
* @return {undefined}
*/
FieldAggParam.prototype.write = function (aggConfig, output) {
output.params.field = aggConfig.params.field.name;
};
return FieldAggParam;
};
});

View file

@ -0,0 +1,56 @@
define(function (require) {
return function OptionedAggParamFactory(Private) {
var _ = require('lodash');
var Registry = require('utils/registry/registry');
var editorHtml = require('text!components/agg_types/controls/field.html');
var BaseAggParam = Private(require('components/agg_types/param_types/base'));
_(OptionedAggParam).inherits(BaseAggParam);
function OptionedAggParam(config) {
OptionedAggParam.Super.call(this, config);
this.options = new Registry({
index: ['val'],
immutable: true,
initialSet: this.options
});
}
/**
* Serialize a selection to be stored in the database
* @param {object} selected - the option that was selected
* @return {any}
*/
OptionedAggParam.prototype.serialize = function (selected) {
return selected.val;
};
/**
* Take a value that was serialized to the database and
* return the option that is represents
*
* @param {any} val - the value that was saved
* @return {object}
*/
OptionedAggParam.prototype.deserialize = function (val) {
return this.options.byVal[val];
};
/**
* Write the aggregation parameter.
*
* @param {AggConfig} aggConfig - the entire configuration for this agg
* @param {object} output - the result of calling write on all of the aggregations
* parameters.
* @param {object} output.param - the final object that will be included as the params
* for the agg
* @return {undefined}
*/
OptionedAggParam.prototype.write = function (aggConfig, output) {
output.params[this.name] = aggConfig.params[this.name].val;
};
return OptionedAggParam;
};
});

View file

@ -16,6 +16,8 @@ define(function (require) {
'csv:separator': ',',
'csv:quoteValues': true,
'history:limit': 10
'history:limit': 10,
'shortDots:enable': false
});
});

View file

@ -4,6 +4,11 @@ define(function (require) {
var module = require('modules').get('kibana');
var template = require('text!components/filter_bar/filter_bar.html');
var mapFilter = require('./lib/mapFilter');
var toggleFilter = require('./lib/toggleFilter');
var removeFilter = require('./lib/removeFilter');
var removeAll = require('./lib/removeAll');
module.directive('filterBar', function (courier) {
return {
restrict: 'E',
@ -13,33 +18,6 @@ define(function (require) {
},
link: function ($scope, $el, attrs) {
/**
* Map the filter into an object with the key and value exposed so it's
* easier to work with in the template
* @param {object} fitler The filter the map
* @returns {object}
*/
var mapFilter = function (filter) {
var key, value;
if (filter.query) {
key = _.keys(filter.query.match)[0];
value = filter.query.match[key].query;
} else if (filter.exists) {
key = 'exists';
value = filter.exists.field;
} else if (filter.missing) {
key = 'missing';
value = filter.missing.field;
}
return {
key: key,
value: value,
disabled: !!(filter.disabled),
negate: !!(filter.negate),
filter: filter
};
};
$scope.$watch('state.filters', function (filters) {
// Get the filters from the searchSource
$scope.filters = _(filters)
@ -52,54 +30,9 @@ define(function (require) {
});
/**
* Remap the filter from the intermediary back to it's original.
* @param {object} filter The original filter
* @returns {object}
*/
var remapFilters = function (filter) {
return filter.filter;
};
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
* @returns {void}
*/
$scope.toggleFilter = function (filter) {
// Toggle the disabled flag
var disabled = !filter.disabled;
filter.disabled = disabled;
filter.filter.disabled = disabled;
// Save the filters back to the searchSource
$scope.state.filters = _.map($scope.filters, remapFilters);
};
/**
* Removes the filter from the searchSource
* @param {object} filter The filter to remove
* @returns {void}
*/
$scope.removeFilter = function (invalidFilter) {
// Remove the filter from the the scope $filters and map it back
// to the original format to save in searchSource
$scope.state.filters = _($scope.filters)
.filter(function (filter) {
return filter.filter !== invalidFilter.filter;
})
.map(remapFilters)
.value();
};
/**
* Removes all filters
* @returns {void}
*/
$scope.removeAll = function () {
$scope.state.filters = [];
};
$scope.toggleFilter = toggleFilter($scope);
$scope.removeFilter = removeFilter($scope);
$scope.removeAll = removeAll($scope);
}
};
});

View file

@ -0,0 +1,30 @@
define(function (require) {
var _ = require('lodash');
/**
* Map the filter into an object with the key and value exposed so it's
* easier to work with in the template
* @param {object} fitler The filter the map
* @returns {object}
*/
return function (filter) {
var key, value;
if (filter.query) {
key = _.keys(filter.query.match)[0];
value = filter.query.match[key].query;
} else if (filter.exists) {
key = 'exists';
value = filter.exists.field;
} else if (filter.missing) {
key = 'missing';
value = filter.missing.field;
}
return {
key: key,
value: value,
disabled: !!(filter.disabled),
negate: !!(filter.negate),
filter: filter
};
};
});

View file

@ -0,0 +1,12 @@
define(function (require) {
'use strict';
/**
* Remap the filter from the intermediary back to it's original.
* @param {object} filter The original filter
* @returns {object}
*/
return function (filter) {
return filter.filter;
};
});

View file

@ -0,0 +1,11 @@
define(function (require) {
return function ($scope) {
/**
* Removes all filters
* @returns {void}
*/
return function () {
$scope.state.filters = [];
};
};
});

View file

@ -0,0 +1,22 @@
define(function (require) {
var _ = require('lodash');
var remapFilters = require('./remapFilters');
return function ($scope) {
/**
* Removes the filter from the searchSource
* @param {object} filter The filter to remove
* @returns {void}
*/
return function (invalidFilter) {
// Remove the filter from the the scope $filters and map it back
// to the original format to save in searchSource
$scope.state.filters = _($scope.filters)
.filter(function (filter) {
return filter.filter !== invalidFilter.filter;
})
.map(remapFilters)
.value();
};
};
});

View file

@ -0,0 +1,20 @@
define(function (require) {
var _ = require('lodash');
var remapFilters = require('./remapFilters');
return function ($scope) {
/**
* Toggles the filter between enabled/disabled.
* @param {object} filter The filter to toggle
* @returns {void}
*/
return function (filter) {
// Toggle the disabled flag
var disabled = !filter.disabled;
filter.disabled = disabled;
filter.filter.disabled = disabled;
// Save the filters back to the searchSource
$scope.state.filters = _.map($scope.filters, remapFilters);
};
};
});

View file

@ -2,7 +2,7 @@
/* markdown
### Formatting a value
To format a response value, you need to get ahold of the field list, which is usually available at `indexPattern.fields` or `indexPattern.fieldsByName`. When the indexPattern is not available, call `courier.getFieldsFor`. Each field object has a `format` property*, which is an object detailed in [_field_formats.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_field_formats.js).
To format a response value, you need to get ahold of the field list, which is usually available at `indexPattern.fields`. When the indexPattern is not available, call `courier.getFieldsFor`. Each field object has a `format` property*, which is an object detailed in [_field_formats.js](https://github.com/elasticsearch/kibana4/blob/master/src/kibana/components/index_patterns/_field_formats.js).
Once you have the field that a response value came from, pass the value to `field.format.convert(value)` and a formatted string representation of the field will be returned.

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