mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
resolving conflict with master
This commit is contained in:
commit
c1fe26e314
154 changed files with 6282 additions and 2838 deletions
18
TODOS.md
18
TODOS.md
|
@ -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**
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'"
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
|
@ -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) {
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
||||
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>
|
79
src/kibana/apps/visualize/editor/agg.html
Normal file
79
src/kibana/apps/visualize/editor/agg.html
Normal 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>
|
163
src/kibana/apps/visualize/editor/agg.js
Normal file
163
src/kibana/apps/visualize/editor/agg.js
Normal 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);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
48
src/kibana/apps/visualize/editor/agg_group.html
Normal file
48
src/kibana/apps/visualize/editor/agg_group.html
Normal 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>
|
72
src/kibana/apps/visualize/editor/agg_group.js
Normal file
72
src/kibana/apps/visualize/editor/agg_group.js
Normal 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);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
18
src/kibana/apps/visualize/editor/agg_param.js
Normal file
18
src/kibana/apps/visualize/editor/agg_param.js
Normal 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();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
87
src/kibana/apps/visualize/editor/editor.html
Normal file
87
src/kibana/apps/visualize/editor/editor.html
Normal 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>
|
||||
|
||||
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>
|
208
src/kibana/apps/visualize/editor/editor.js
Normal file
208
src/kibana/apps/visualize/editor/editor.js
Normal 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();
|
||||
});
|
||||
});
|
202
src/kibana/apps/visualize/editor/editor.less
Normal file
202
src/kibana/apps/visualize/editor/editor.less
Normal 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;
|
||||
}
|
||||
}
|
90
src/kibana/apps/visualize/editor/nesting_indicator.js
Normal file
90
src/kibana/apps/visualize/editor/nesting_indicator.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
|
@ -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>
|
11
src/kibana/apps/visualize/editor/panels/share.html
Normal file
11
src/kibana/apps/visualize/editor/panels/share.html
Normal 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><iframe src="{{conf.shareData().embed}}" height="400" width="600"></iframe></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Share a link</label>
|
||||
<div class="form-control" disabled>{{conf.shareData().link}}</div>
|
||||
</div>
|
||||
</form>
|
46
src/kibana/apps/visualize/editor/sidebar.html
Normal file
46
src/kibana/apps/visualize/editor/sidebar.html
Normal 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>
|
21
src/kibana/apps/visualize/editor/sidebar.js
Normal file
21
src/kibana/apps/visualize/editor/sidebar.js
Normal 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 |
|
@ -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', {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,9 +0,0 @@
|
|||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
ng-model="config.global"
|
||||
type="checkbox">Run First
|
||||
|
||||
<kbn-info info="Ensure that all charts have the same {{group.label | lowercase}} values." placement="right"></kbn-info>
|
||||
</label>
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
<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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}}
|
||||
<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>
|
|
@ -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><iframe src="{{conf.shareData().embed}}" height="400" width="600"></iframe></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Share a link</label>
|
||||
<div class="form-control" disabled>{{conf.shareData().link}}</div>
|
||||
</div>
|
||||
</form>
|
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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'))
|
||||
];
|
||||
};
|
||||
});
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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: '*'}}}
|
||||
];
|
||||
}))
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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);
|
||||
}, [])
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
|
@ -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";
|
|
@ -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>
|
|
@ -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) {
|
18
src/kibana/components/agg_types/README.md
Normal file
18
src/kibana/components/agg_types/README.md
Normal 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
|
46
src/kibana/components/agg_types/_agg_params.js
Normal file
46
src/kibana/components/agg_types/_agg_params.js
Normal 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;
|
||||
};
|
||||
});
|
29
src/kibana/components/agg_types/_agg_type.js
Normal file
29
src/kibana/components/agg_types/_agg_type.js
Normal 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;
|
||||
};
|
||||
});
|
50
src/kibana/components/agg_types/buckets/_interval_options.js
Normal file
50
src/kibana/components/agg_types/buckets/_interval_options.js
Normal 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')
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
117
src/kibana/components/agg_types/buckets/date_histogram.js
Normal file
117
src/kibana/components/agg_types/buckets/date_histogram.js
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
36
src/kibana/components/agg_types/buckets/filters.js
Normal file
36
src/kibana/components/agg_types/buckets/filters.js
Normal 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: '*'} }
|
||||
};
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
68
src/kibana/components/agg_types/buckets/histogram.js
Normal file
68
src/kibana/components/agg_types/buckets/histogram.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
34
src/kibana/components/agg_types/buckets/range.js
Normal file
34
src/kibana/components/agg_types/buckets/range.js
Normal 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;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
24
src/kibana/components/agg_types/buckets/significant_terms.js
Normal file
24
src/kibana/components/agg_types/buckets/significant_terms.js
Normal 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'),
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
40
src/kibana/components/agg_types/buckets/terms.js
Normal file
40
src/kibana/components/agg_types/buckets/terms.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
});
|
|
@ -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>
|
17
src/kibana/components/agg_types/controls/field.html
Normal file
17
src/kibana/components/agg_types/controls/field.html
Normal 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>
|
24
src/kibana/components/agg_types/controls/filters.html
Normal file
24
src/kibana/components/agg_types/controls/filters.html
Normal 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>
|
18
src/kibana/components/agg_types/controls/interval.html
Normal file
18
src/kibana/components/agg_types/controls/interval.html
Normal 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>
|
10
src/kibana/components/agg_types/controls/min_doc_count.html
Normal file
10
src/kibana/components/agg_types/controls/min_doc_count.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<div class="checkbox ng-scope">
|
||||
<label>
|
||||
<input ng-model="params.min_doc_count" type="checkbox">
|
||||
Show empty buckets
|
||||
<kbn-info
|
||||
info="Show all buckets, not only the buckets with results."
|
||||
placement="right">
|
||||
</kbn-info>
|
||||
</label>
|
||||
</div>
|
21
src/kibana/components/agg_types/controls/order_and_size.html
Normal file
21
src/kibana/components/agg_types/controls/order_and_size.html
Normal 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>
|
39
src/kibana/components/agg_types/controls/ranges.html
Normal file
39
src/kibana/components/agg_types/controls/ranges.html
Normal 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>
|
29
src/kibana/components/agg_types/index.js
Normal file
29
src/kibana/components/agg_types/index.js
Normal 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)
|
||||
});
|
||||
};
|
||||
});
|
81
src/kibana/components/agg_types/metric_aggs.js
Normal file
81
src/kibana/components/agg_types/metric_aggs.js
Normal 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);
|
||||
});
|
||||
};
|
||||
});
|
11
src/kibana/components/agg_types/param_types/base.js
Normal file
11
src/kibana/components/agg_types/param_types/base.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
define(function (require) {
|
||||
return function BaseAggParamFactory() {
|
||||
var _ = require('lodash');
|
||||
|
||||
function BaseAggParam(config) {
|
||||
_.assign(this, config);
|
||||
}
|
||||
|
||||
return BaseAggParam;
|
||||
};
|
||||
});
|
54
src/kibana/components/agg_types/param_types/field.js
Normal file
54
src/kibana/components/agg_types/param_types/field.js
Normal 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;
|
||||
};
|
||||
});
|
56
src/kibana/components/agg_types/param_types/optioned.js
Normal file
56
src/kibana/components/agg_types/param_types/optioned.js
Normal 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;
|
||||
};
|
||||
});
|
|
@ -16,6 +16,8 @@ define(function (require) {
|
|||
'csv:separator': ',',
|
||||
'csv:quoteValues': true,
|
||||
|
||||
'history:limit': 10
|
||||
'history:limit': 10,
|
||||
|
||||
'shortDots:enable': false
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
30
src/kibana/components/filter_bar/lib/mapFilter.js
Normal file
30
src/kibana/components/filter_bar/lib/mapFilter.js
Normal 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
|
||||
};
|
||||
};
|
||||
});
|
12
src/kibana/components/filter_bar/lib/remapFilters.js
Normal file
12
src/kibana/components/filter_bar/lib/remapFilters.js
Normal 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;
|
||||
};
|
||||
|
||||
});
|
11
src/kibana/components/filter_bar/lib/removeAll.js
Normal file
11
src/kibana/components/filter_bar/lib/removeAll.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
define(function (require) {
|
||||
return function ($scope) {
|
||||
/**
|
||||
* Removes all filters
|
||||
* @returns {void}
|
||||
*/
|
||||
return function () {
|
||||
$scope.state.filters = [];
|
||||
};
|
||||
};
|
||||
});
|
22
src/kibana/components/filter_bar/lib/removeFilter.js
Normal file
22
src/kibana/components/filter_bar/lib/removeFilter.js
Normal 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();
|
||||
};
|
||||
|
||||
};
|
||||
});
|
20
src/kibana/components/filter_bar/lib/toggleFilter.js
Normal file
20
src/kibana/components/filter_bar/lib/toggleFilter.js
Normal 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);
|
||||
};
|
||||
};
|
||||
});
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue