@spalger/kibana
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
539 lines (450 loc) • 16.7 kB
JavaScript
define(function (require) {
var _ = require('lodash');
var angular = require('angular');
var moment = require('moment');
var ConfigTemplate = require('ui/ConfigTemplate');
var getSort = require('ui/doc_table/lib/get_sort');
var rison = require('ui/utils/rison');
var dateMath = require('ui/utils/dateMath');
require('ui/doc_table');
require('ui/visualize');
require('ui/notify');
require('ui/timepicker');
require('ui/fixedScroll');
require('ui/directives/validate_json');
require('ui/validate_query');
require('ui/filters/moment');
require('ui/courier');
require('ui/index_patterns');
require('ui/state_management/app_state');
require('ui/timefilter');
require('ui/highlight/highlight_tags');
var app = require('ui/modules').get('apps/discover', [
'kibana/notify',
'kibana/courier',
'kibana/index_patterns'
]);
require('ui/routes')
.when('/discover/:id?', {
template: require('plugins/kibana/discover/index.html'),
reloadOnSearch: false,
resolve: {
ip: function (Promise, courier, config, $location) {
return courier.indexPatterns.getIds()
.then(function (list) {
var stateRison = $location.search()._a;
var state;
try { state = rison.decode(stateRison); }
catch (e) { state = {}; }
var specified = !!state.index;
var exists = _.contains(list, state.index);
var id = exists ? state.index : config.get('defaultIndex');
return Promise.props({
list: list,
loaded: courier.indexPatterns.get(id),
stateVal: state.index,
stateValFound: specified && exists
});
});
},
savedSearch: function (courier, savedSearches, $route) {
return savedSearches.get($route.current.params.id)
.catch(courier.redirectWhenMissing({
'search': '/discover',
'index-pattern': '/settings/objects/savedSearches/' + $route.current.params.id
}));
}
}
});
app.controller('discover', function ($scope, config, courier, $route, $window, Notifier,
AppState, timefilter, Promise, Private, kbnUrl, highlightTags) {
var Vis = Private(require('ui/Vis'));
var docTitle = Private(require('ui/doc_title'));
var brushEvent = Private(require('ui/utils/brush_event'));
var HitSortFn = Private(require('plugins/kibana/discover/_hit_sort_fn'));
var queryFilter = Private(require('ui/filter_bar/query_filter'));
var filterManager = Private(require('ui/filter_manager'));
var notify = new Notifier({
location: 'Discover'
});
$scope.intervalOptions = Private(require('ui/agg_types/buckets/_interval_options'));
$scope.showInterval = false;
$scope.intervalEnabled = function (interval) {
return interval.val !== 'custom';
};
$scope.toggleInterval = function () {
$scope.showInterval = !$scope.showInterval;
};
// config panel templates
$scope.configTemplate = new ConfigTemplate({
load: require('plugins/kibana/discover/partials/load_search.html'),
save: require('plugins/kibana/discover/partials/save_search.html')
});
$scope.timefilter = timefilter;
// the saved savedSearch
var savedSearch = $route.current.locals.savedSearch;
$scope.$on('$destroy', savedSearch.destroy);
// the actual courier.SearchSource
$scope.searchSource = savedSearch.searchSource;
$scope.indexPattern = resolveIndexPatternLoading();
$scope.searchSource.set('index', $scope.indexPattern);
if (savedSearch.id) {
docTitle.change(savedSearch.title);
}
var $state = $scope.state = new AppState(getStateDefaults());
function getStateDefaults() {
return {
query: $scope.searchSource.get('query') || '',
sort: getSort.array(savedSearch.sort, $scope.indexPattern),
columns: savedSearch.columns || ['_source'],
index: $scope.indexPattern.id,
interval: 'auto',
filters: _.cloneDeep($scope.searchSource.getOwn('filter'))
};
}
$state.index = $scope.indexPattern.id;
$state.sort = getSort.array($state.sort, $scope.indexPattern);
$scope.$watchCollection('state.columns', function () {
$state.save();
});
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: config.get('discover:sampleSize'),
// Index to match
index: $scope.indexPattern.id,
timefield: $scope.indexPattern.timeFieldName,
savedSearch: savedSearch,
indexPatternList: $route.current.locals.ip.list
};
var init = _.once(function () {
var showTotal = 5;
$scope.failuresShown = showTotal;
$scope.showAllFailures = function () {
$scope.failuresShown = $scope.failures.length;
};
$scope.showLessFailures = function () {
$scope.failuresShown = showTotal;
};
$scope.updateDataSource()
.then(function () {
$scope.$listen(timefilter, 'fetch', function () {
$scope.fetch();
});
$scope.$watchCollection('state.sort', function (sort) {
if (!sort) return;
// get the current sort from {key: val} to ["key", "val"];
var currentSort = _.pairs($scope.searchSource.get('sort')).pop();
// if the searchSource doesn't know, tell it so
if (!angular.equals(sort, currentSort)) $scope.fetch();
});
// update data source when filters update
$scope.$listen(queryFilter, 'update', function () {
return $scope.updateDataSource().then(function () {
$state.save();
});
});
// update data source when hitting forward/back and the query changes
$scope.$listen($state, 'fetch_with_changes', function (diff) {
if (diff.indexOf('query') >= 0) $scope.fetch();
});
// fetch data when filters fire fetch event
$scope.$listen(queryFilter, 'fetch', $scope.fetch);
$scope.$watch('opts.timefield', function (timefield) {
timefilter.enabled = !!timefield;
});
$scope.$watch('state.interval', function (interval, oldInterval) {
if (interval !== oldInterval && interval === 'auto') {
$scope.showInterval = false;
}
$scope.fetch();
});
$scope.$watch('vis.aggs', function () {
var buckets = $scope.vis.aggs.bySchemaGroup.buckets;
if (buckets && buckets.length === 1) {
$scope.intervalName = 'by ' + buckets[0].buckets.getInterval().description;
} else {
$scope.intervalName = 'auto';
}
});
$scope.$watchMulti([
'rows',
'fetchStatus'
], (function updateResultState() {
var prev = {};
var status = {
LOADING: 'loading', // initial data load
READY: 'ready', // results came back
NO_RESULTS: 'none' // no results came back
};
function pick(rows, oldRows, fetchStatus) {
// initial state, pretend we are loading
if (rows == null && oldRows == null) return status.LOADING;
var rowsEmpty = _.isEmpty(rows);
if (rowsEmpty && fetchStatus) return status.LOADING;
else if (!rowsEmpty) return status.READY;
else return status.NO_RESULTS;
}
return function () {
var current = {
rows: $scope.rows,
fetchStatus: $scope.fetchStatus
};
$scope.resultState = pick(
current.rows,
prev.rows,
current.fetchStatus,
prev.fetchStatus
);
prev = current;
};
}()));
$scope.searchSource.onError(function (err) {
console.log(err);
notify.error('An error occurred with your request. Reset your inputs and try again.');
}).catch(notify.fatal);
function initForTime() {
return setupVisualization().then($scope.updateTime);
}
return Promise.resolve($scope.opts.timefield && initForTime())
.then(function () {
init.complete = true;
$state.replace();
$scope.$emit('application.load');
});
});
});
$scope.opts.saveDataSource = function () {
return $scope.updateDataSource()
.then(function () {
savedSearch.id = savedSearch.title;
savedSearch.columns = $scope.state.columns;
savedSearch.sort = $scope.state.sort;
return savedSearch.save()
.then(function (id) {
$scope.configTemplate.close('save');
if (id) {
notify.info('Saved Data Source "' + savedSearch.title + '"');
if (savedSearch.id !== $route.current.params.id) {
kbnUrl.change('/discover/{{id}}', { id: savedSearch.id });
} else {
// Update defaults so that "reload saved query" functions correctly
$state.setDefaults(getStateDefaults());
}
}
});
})
.catch(notify.error);
};
$scope.opts.fetch = $scope.fetch = function () {
// ignore requests to fetch before the app inits
if (!init.complete) return;
$scope.updateTime();
$scope.updateDataSource()
.then(setupVisualization)
.then(function () {
$state.save();
return courier.fetch();
})
.catch(notify.error);
};
$scope.searchSource.onBeginSegmentedFetch(function (segmented) {
function flushResponseData() {
$scope.hits = 0;
$scope.faliures = [];
$scope.rows = [];
$scope.fieldCounts = {};
}
if (!$scope.rows) flushResponseData();
var sort = $state.sort;
var timeField = $scope.indexPattern.timeFieldName;
var totalSize = $scope.size || $scope.opts.sampleSize;
/**
* Basically an emum.
*
* opts:
* "time" - sorted by the timefield
* "non-time" - explicitly sorted by a non-time field, NOT THE SAME AS `sortBy !== "time"`
* "implicit" - no sorting set, NOT THE SAME AS "non-time"
*
* @type {String}
*/
var sortBy = (function () {
if (!_.isArray(sort)) return 'implicit';
else if (sort[0] === timeField) return 'time';
else return 'non-time';
}());
var sortFn = null;
if (sortBy === 'non-time') {
sortFn = new HitSortFn(sort[1]);
}
$scope.updateTime();
segmented.setDirection(sortBy === 'time' ? (sort[1] || 'desc') : 'desc');
segmented.setSize(sortBy === 'time' ? $scope.opts.sampleSize : false);
// triggered when the status updated
segmented.on('status', function (status) {
$scope.fetchStatus = status;
});
segmented.on('first', function () {
flushResponseData();
});
segmented.on('segment', notify.timed('handle each segment', function (resp) {
if (resp._shards.failed > 0) {
$scope.failures = _.union($scope.failures, resp._shards.failures);
$scope.failures = _.uniq($scope.failures, false, function (failure) {
return failure.index + failure.shard + failure.reason;
});
}
var rows = $scope.rows;
var indexPattern = $scope.searchSource.get('index');
// merge the rows and the hits, use a new array to help watchers
rows = $scope.rows = rows.concat(resp.hits.hits);
if (sortFn) {
notify.event('resort rows', function () {
rows.sort(sortFn);
rows = $scope.rows = rows.slice(0, totalSize);
$scope.fieldCounts = {};
});
}
notify.event('flatten hit and count fields', function () {
var counts = $scope.fieldCounts;
$scope.rows.forEach(function (hit) {
// skip this work if we have already done it and we are NOT sorting.
// ---
// when we are sorting results, we need to redo the counts each time because the
// "top 500" may change with each response
if (hit.$$_counted && !sortFn) return;
hit.$$_counted = true;
var fields = _.keys(indexPattern.flattenHit(hit));
var n = fields.length;
var field;
while (field = fields[--n]) {
if (counts[field]) counts[field] += 1;
else counts[field] = 1;
}
});
});
}));
segmented.on('mergedSegment', function (merged) {
$scope.mergedEsResp = merged;
$scope.hits = merged.hits.total;
});
segmented.on('complete', function () {
if ($scope.fetchStatus.hitCount === 0) {
flushResponseData();
}
$scope.fetchStatus = null;
});
}).catch(notify.fatal);
$scope.updateTime = function () {
$scope.timeRange = {
from: dateMath.parse(timefilter.time.from),
to: dateMath.parse(timefilter.time.to, true)
};
};
$scope.resetQuery = function () {
kbnUrl.change('/discover/{{id}}', { id: $route.current.params.id });
};
$scope.newQuery = function () {
kbnUrl.change('/discover');
};
$scope.updateDataSource = Promise.method(function () {
$scope.searchSource
.size($scope.opts.sampleSize)
.sort(getSort($state.sort, $scope.indexPattern))
.query(!$state.query ? null : $state.query)
.highlight({
pre_tags: [highlightTags.pre],
post_tags: [highlightTags.post],
fields: {'*': {}},
fragment_size: 2147483647 // Limit of an integer.
})
.set('filter', queryFilter.getFilters());
});
// TODO: On array fields, negating does not negate the combination, rather all terms
$scope.filterQuery = function (field, values, operation) {
$scope.indexPattern.popularizeField(field, 1);
filterManager.add(field, values, operation, $state.index);
};
$scope.toTop = function () {
$window.scrollTo(0, 0);
};
var loadingVis;
function setupVisualization() {
// If we're not setting anything up we need to return an empty promise
if (!$scope.opts.timefield) return Promise.resolve();
if (loadingVis) return loadingVis;
var visStateAggs = [
{
type: 'count',
schema: 'metric'
},
{
type: 'date_histogram',
schema: 'segment',
params: {
field: $scope.opts.timefield,
interval: $state.interval,
min_doc_count: 0
}
}
];
// we have a vis, just modify the aggs
if ($scope.vis) {
var visState = $scope.vis.getState();
visState.aggs = visStateAggs;
$scope.vis.setState(visState);
return Promise.resolve($scope.vis);
}
$scope.vis = new Vis($scope.indexPattern, {
type: 'histogram',
params: {
addLegend: false,
addTimeMarker: true
},
listeners: {
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';
},
brush: brushEvent
},
aggs: visStateAggs
});
$scope.searchSource.aggs(function () {
$scope.vis.requesting();
return $scope.vis.aggs.toDsl();
});
// stash this promise so that other calls to setupVisualization will have to wait
loadingVis = new Promise(function (resolve) {
$scope.$on('ready:vis', function () {
resolve($scope.vis);
});
})
.finally(function () {
// clear the loading flag
loadingVis = null;
});
return loadingVis;
}
function resolveIndexPatternLoading() {
var props = $route.current.locals.ip;
var loaded = props.loaded;
var stateVal = props.stateVal;
var stateValFound = props.stateValFound;
var own = $scope.searchSource.getOwn('index');
if (own && !stateVal) return own;
if (stateVal && !stateValFound) {
var err = '"' + stateVal + '" is not a configured pattern. ';
if (own) {
notify.warning(err + ' Using the saved index pattern: "' + own.id + '"');
return own;
}
notify.warning(err + ' Using the default index pattern: "' + loaded.id + '"');
}
return loaded;
}
init();
});
});