added saved searches (saved dataSources)

This commit is contained in:
Spencer Alger 2014-03-24 15:41:38 -07:00
parent aafa3d3af1
commit e7eb964c44
13 changed files with 488 additions and 330 deletions

View file

@ -6,22 +6,22 @@ define(function (require) {
var nextTick = require('utils/next_tick');
function DataSource(courier, initialState) {
var state;
EventEmitter.call(this);
// state can be serialized as JSON, and passed back in to restore
if (initialState) {
if (typeof initialState === 'string') {
state = JSON.parse(initialState);
this._state = (function () {
// state can be serialized as JSON, and passed back in to restore
if (initialState) {
if (typeof initialState === 'string') {
return JSON.parse(initialState);
} else {
return _.cloneDeep(initialState);
}
} else {
state = _.cloneDeep(initialState);
return {};
}
} else {
state = {};
}
}());
this._state = state;
this._dynamicState = this._dynamicState || {};
this._courier = courier;
// before newListener to prevent unnecessary "emit" when added
@ -64,9 +64,9 @@ define(function (require) {
this._methods.forEach(function (name) {
this[name] = function (val) {
if (val == null) {
delete state[name];
delete this._state[name];
} else {
state[name] = val;
this._state[name] = val;
}
return this;
@ -87,6 +87,7 @@ define(function (require) {
var current = this;
while (current) {
if (current._state[name] !== void 0) return current._state[name];
if (current._dynamicState[name] !== void 0) return current._dynamicState[name]();
current = current._parent;
}
};
@ -108,7 +109,7 @@ define(function (require) {
* Clear the disabled flag, you do not need to call this unless you
* explicitly disabled the DataSource
*/
DataSource.prototype.enableFetch = function () {
DataSource.prototype.enableAuthFetch = function () {
delete this._fetchDisabled;
return this;
};
@ -116,7 +117,7 @@ define(function (require) {
/**
* Disable the DataSource, preventing it or any of it's children from being searched
*/
DataSource.prototype.disableFetch = function () {
DataSource.prototype.disableAutoFetch = function () {
this._fetchDisabled = true;
return this;
};

View file

@ -210,16 +210,16 @@ define(function (require) {
* @param {Function} cb - callback
*/
DocSource.prototype._sendToEs = function (method, validateVersion, body, cb) {
cb = this._wrapcb(cb)
var source = this;
var courier = this._courier;
var client = courier._getClient();
var params = {
id: this._state.id,
type: this._state.type,
index: this._state.index,
body: body,
ignore: [409]
};
// straight assignment will causes undefined values
var params = _.pick(this._state, 'id', 'type', 'index');
params.body = body;
params.ignore = [409];
if (validateVersion) {
params.version = source._getVersion();

View file

@ -0,0 +1,217 @@
define(function (require) {
var _ = require('utils/mixins');
var settingsHtml = require('text!../partials/settings.html');
var app = require('modules').get('app/discover');
var intervals = [
{ display: '', val: null },
{ display: 'Hourly', val: 'hourly' },
{ display: 'Daily', val: 'daily' },
{ display: 'Weekly', val: 'weekly' },
{ display: 'Monthly', val: 'monthly' },
{ display: 'Yearly', val: 'yearly' }
];
app.controller('discover', function ($scope, config, $q, $route, savedSearches, courier, createNotifier, $location) {
var notify = createNotifier({
location: 'Discover'
});
var search = $route.current.locals.search;
if (!search) return notify.fatal('search failed to load');
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: 500,
// max length for summaries in the table
maxSummaryLength: 100,
// Index to match
index: 'logstash-*',
timefield: '@timestamp',
savedSearch: search
};
// track the initial state of the search
var searchIsPhantom = search.phantom;
$scope.opts.saveDataSource = function () {
search.save()
.then(function () {
notify.info('Saved Data Source "' + search.details.name + '"');
if (searchIsPhantom) {
searchIsPhantom = false;
$location.url('/discover/' + search.get('id'));
}
}, notify.error);
};
// stores the complete list of fields
$scope.fields = null;
// stores the fields we want to fetch
$scope.columns = null;
// index pattern interval options
$scope.intervals = intervals;
$scope.interval = intervals[0];
var initialQuery = search.get('query');
$scope.query = initialQuery ? initialQuery.query_string.query : '';
// the index to use when they don't specify one
config.$watch('discover.defaultIndex', function (val) {
if (!val) return config.set('discover.defaultIndex', '_all');
if (!$scope.opts.index) {
$scope.opts.index = val;
$scope.fetch();
}
});
search
.$scope($scope)
.inherits(courier.rootSearchSource)
.on('results', function (res) {
if (!$scope.fields) getFields();
$scope.rows = res.hits.hits;
});
$scope.sort = ['_score', 'desc'];
$scope.getSort = function () {
return $scope.sort;
};
$scope.setSort = function (field, order) {
var sort = {};
sort[field] = order;
search.sort([sort]);
$scope.sort = [field, order];
$scope.fetch();
};
$scope.toggleConfig = function () {
// Close if already open
if ($scope.configTemplate === settingsHtml) {
delete $scope.configTemplate;
} else {
$scope.configTemplate = settingsHtml;
}
};
$scope.fetch = function () {
if (!$scope.fields) getFields();
search
.size($scope.opts.sampleSize)
.query(!$scope.query ? null : {
query_string: {
query: $scope.query
}
});
if ($scope.opts.index !== search.get('index')) {
// set the index on the savedSearch
search.index($scope.opts.index);
// clear the columns and fields, then refetch when we do a search
$scope.columns = $scope.fields = null;
}
// fetch just this savedSearch
search.fetch();
};
var activeGetFields;
function getFields() {
var defer = $q.defer();
if (activeGetFields) {
activeGetFields.then(function () {
defer.resolve();
});
return;
}
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
display: field.display
};
}, {});
search
.getFields()
.then(function (fields) {
if (!fields) return;
$scope.fields = [];
$scope.columns = $scope.columns || [];
// Inject source into list;
$scope.fields.push({name: '_source', type: 'source', display: false});
_(fields)
.keys()
.sort()
.each(function (name) {
var field = fields[name];
field.name = name;
_.defaults(field, currentState[name]);
$scope.fields.push(field);
});
refreshColumns();
defer.resolve();
}, defer.reject);
return defer.promise.then(function () {
activeGetFields = null;
});
}
$scope.toggleField = function (name) {
var field = _.find($scope.fields, { name: name });
// toggle the display property
field.display = !field.display;
if ($scope.columns.length === 1 && $scope.columns[0] === '_source') {
$scope.columns = _.toggleInOut($scope.columns, name);
$scope.columns = _.toggleInOut($scope.columns, '_source');
_.find($scope.fields, {name: '_source'}).display = false;
} else {
$scope.columns = _.toggleInOut($scope.columns, name);
}
refreshColumns();
};
$scope.refreshFieldList = function () {
search.clearFieldCache(function () {
getFields(function () {
$scope.fetch();
});
});
};
function refreshColumns() {
// Get all displayed field names;
var fields = _.pluck(_.filter($scope.fields, function (field) {
return field.display;
}), 'name');
// Make sure there are no columns added that aren't in the displayed field list.
$scope.columns = _.intersection($scope.columns, fields);
// If no columns remain, use _source
if (!$scope.columns.length) {
$scope.toggleField('_source');
}
}
$scope.$emit('application.load');
});
});

View file

@ -0,0 +1,116 @@
define(function (require) {
var app = require('modules').get('app/discover');
var bind = require('lodash').bind;
var assign = require('lodash').assign;
var nextTick = require('utils/next_tick');
app.factory('SavedSearch', function (configFile, courier, $q) {
function SavedSearch(id) {
var search = courier.createSource('search');
search._doc = courier.createSource('doc')
.index(configFile.kibanaIndex)
.type('saved_searches')
.id(id || void 0)
.on('results', function onResults(resp) {
if (!resp.found) {
search._doc.removeListener('results', onResults);
search.emit('noconfig', new Error('Unable to find that Saved Search...'));
}
search.set(resp._source.state);
search.details = resp._source.details;
assign(search.deatils, resp._source.details);
if (!id) {
id = resp._id;
// it's no longer a phantom
search.phantom = false;
}
if (!search.ready()) {
search.ready(true);
// allow the search to be fetched automatically
search.enableAuthFetch();
}
});
search._dynamicState.id = function () {
return search._doc.get('id');
};
search.phantom = true;
search.details = {
name: '',
hits: 0
};
search.ready = (function () {
var queue = id ? [] : false;
var err;
return function (cb) {
switch (typeof cb) {
// check if we are ready yet
case 'undefined':
return !queue;
// call or queue a function once ready
case 'function':
if (queue) {
// queue will be false once complete
queue.push(cb);
} else {
// always callback async
nextTick(cb, err);
}
return;
// indicate that we are ready, or there was a failure loading
default:
if (queue && cb) {
if (cb instanceof Error) {
err = cb;
}
// if queued functions are confused, and ask us if
// we are ready, we should tell them yes
var fns = queue;
queue = false;
// be sure to send out the error we got if there was one
fns.forEach(function (fn) { fn(err); });
}
}
};
}());
search.save = function () {
var defer = $q.defer();
search._doc.doIndex({
details: search.details,
state: search.toJSON()
}, function (err, id) {
if (err) return defer.reject(err);
search._doc.id(id);
defer.resolve();
});
return defer.promise;
};
if (!id) {
// we have nothing left to load
search.ready(true);
} else {
// before this search is fetched, it's config needs to be loaded
search.disableAutoFetch();
// get the config doc now
search._doc.fetch();
}
return search;
}
return SavedSearch;
});
});

View file

@ -1,222 +1,20 @@
define(function (require, module, exports) {
var _ = require('lodash');
require('directives/table');
require('./field_chooser');
require('services/saved_searches');
require('utils/mixins');
require('./services/saved_searches');
require('./timechart');
require('./controllers/discover');
var app = require('modules').get('app/discover');
var intervals = [
{ display: '', val: null },
{ display: 'Hourly', val: 'hourly' },
{ display: 'Daily', val: 'daily' },
{ display: 'Weekly', val: 'weekly' },
{ display: 'Monthly', val: 'monthly' },
{ display: 'Yearly', val: 'yearly' }
];
app.controller('discover', function ($scope, config, $q, $routeParams, savedSearches, courier) {
var source;
if ($routeParams.id) {
source = savedSearches.get($routeParams.id);
} else {
source = savedSearches.create();
}
$scope.opts = {
// number of records to fetch, then paginate through
sampleSize: 500,
// max length for summaries in the table
maxSummaryLength: 100,
// Index to match
index: '_all',
timefield: '@timestamp'
};
// stores the complete list of fields
$scope.fields = null;
// stores the fields we want to fetch
$scope.columns = null;
// index pattern interval options
$scope.intervals = intervals;
$scope.interval = $scope.intervals[0];
// the index to use when they don't specify one
config.$watch('discover.defaultIndex', function (val) {
if (!val) return config.set('discover.defaultIndex', '_all');
if (!$scope.opts.index) {
$scope.opts.index = val;
$scope.fetch();
app.config(function ($routeProvider) {
$routeProvider.when('/discover/:id?', {
templateUrl: 'kibana/apps/discover/index.html',
resolve: {
search: function (savedSearches, $route) {
return savedSearches.get($route.current.params.id);
}
}
});
source
.$scope($scope)
.inherits(courier.rootSearchSource)
.on('results', function (res) {
if (!$scope.fields) getFields();
$scope.rows = res.hits.hits;
});
var init = function () {
$scope.fetch();
};
$scope.sort = ['_score', 'desc'];
$scope.getSort = function () {
return $scope.sort;
};
$scope.setSort = function (field, order) {
var sort = {};
sort[field] = order;
source.sort([sort]);
$scope.sort = [field, order];
$scope.fetch();
};
var setConfigTemplate = function (template) {
// Close if already open
if ($scope.configTemplate === template) {
delete $scope.configTemplate;
return;
} else {
$scope.configTemplate = template;
}
};
$scope.toggleConfig = function () {
setConfigTemplate(require('text!./partials/settings.html'));
/*
$scope.configSubmit = function () {
$scope.save($scope.dashboard.title);
};
*/
};
$scope.fetch = function () {
if (!$scope.fields) getFields();
source
.size($scope.opts.sampleSize)
.query(!$scope.query ? null : {
query_string: {
query: $scope.query
}
});
if ($scope.opts.index !== source.get('index')) {
// set the index on the data source
source.index($scope.opts.index);
// clear the columns and fields, then refetch when we do a search
$scope.columns = $scope.fields = null;
}
// fetch just this datasource
source.fetch();
};
var activeGetFields;
function getFields() {
var defer = $q.defer();
if (activeGetFields) {
activeGetFields.then(function () {
defer.resolve();
});
return;
}
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
display: field.display
};
}, {});
source
.getFields()
.then(function (fields) {
if (!fields) return;
$scope.fields = [];
$scope.columns = $scope.columns || [];
// Inject source into list;
$scope.fields.push({name: '_source', type: 'source', display: false});
_(fields)
.keys()
.sort()
.each(function (name) {
var field = fields[name];
field.name = name;
_.defaults(field, currentState[name]);
$scope.fields.push(field);
});
refreshColumns();
defer.resolve();
}, defer.reject);
return defer.promise.then(function () {
activeGetFields = null;
});
}
$scope.toggleField = function (name) {
var field = _.find($scope.fields, { name: name });
// toggle the display property
field.display = !field.display;
if ($scope.columns.length === 1 && $scope.columns[0] === '_source') {
$scope.columns = _.toggleInOut($scope.columns, name);
$scope.columns = _.toggleInOut($scope.columns, '_source');
_.find($scope.fields, {name: '_source'}).display = false;
} else {
$scope.columns = _.toggleInOut($scope.columns, name);
}
refreshColumns();
};
$scope.refreshFieldList = function () {
source.clearFieldCache(function () {
getFields(function () {
$scope.fetch();
});
});
};
function refreshColumns() {
// Get all displayed field names;
var fields = _.pluck(_.filter($scope.fields, function (field) {
return field.display;
}), 'name');
// Make sure there are no columns added that aren't in the displayed field list.
$scope.columns = _.intersection($scope.columns, fields);
// If no columns remain, use _source
if (!$scope.columns.length) {
$scope.toggleField('_source');
}
}
init();
$scope.$emit('application.load');
});
});

View file

@ -1,7 +1,19 @@
<label class="control-label">Index</label>
<input class="form-control" ng-model="opts.index">
<!--
<label class="control-label">Max Summary Length</label>
<input type="number" name class="form-control" ng-model="opts.maxSummaryLength">
-->
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<label class="control-label">Index</label>
<input class="form-control" ng-model="opts.index">
</div>
<div class="col-md-6">
<div class="form-group">
<label class="control-label">Name</label>
<input ng-model="opts.savedSearch.details.name" class="form-control">
</div>
<div class="form-group">
<button ng-click="opts.saveDataSource()" type="button" class="btn btn-primary">
{{opts.savedSearch.phantom ? 'Create' : 'Save'}}
</button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,25 @@
define(function (require) {
var module = require('modules').get('kibana/services');
var _ = require('lodash');
require('../factories/saved_search');
module.service('savedSearches', function (courier, configFile, $q, createNotifier, SavedSearch) {
var notify = createNotifier({
location: 'Saved Searches'
});
this.get = function (id) {
var defer = $q.defer();
var search = new SavedSearch(id);
search.ready(function (err) {
if (err) defer.reject(err);
else defer.resolve(search);
});
return defer.promise;
};
});
});

View file

@ -10,55 +10,81 @@ define(function (require) {
location: 'Visualize Controller'
});
// the object detailing the visualization
var vis = $scope.vis = window.vis = new Vis({
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
segment: {
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
max: 10
},
split: {
label: 'Rows & Columns',
max: 2
}
}, {
split: [
{
field: 'response',
size: 5,
agg: 'terms'
config: {
metric: {
label: 'Y-Axis',
min: 1,
max: 1
},
{
field: '_type',
size: 5,
agg: 'terms'
segment: {
label: 'X-Axis',
min: 1,
max: 1
},
group: {
label: 'Color',
max: 1
},
split: {
label: 'Rows & Columns',
max: 2
}
],
segment: [
{
field: '@timestamp',
interval: 'week'
}
],
group: [
{
field: 'extension',
size: 5,
agg: 'terms',
global: true
}
]
}
});
// the object detailing the visualization
// var vis = $scope.vis = window.vis = new Vis({
// config: {
// metric: {
// label: 'Y-Axis',
// min: 1,
// max: 1
// },
// segment: {
// label: 'X-Axis',
// min: 1,
// max: 1
// },
// group: {
// label: 'Color',
// max: 10
// },
// split: {
// label: 'Rows & Columns',
// max: 2
// }
// },
// state: {
// split: [
// {
// field: 'response',
// size: 5,
// agg: 'terms'
// },
// {
// field: '_type',
// size: 5,
// agg: 'terms'
// }
// ],
// segment: [
// {
// field: '@timestamp',
// interval: 'week'
// }
// ],
// group: [
// {
// field: 'extension',
// size: 5,
// agg: 'terms',
// global: true
// }
// ]
// }
// });
vis.dataSource.$scope($scope);
$scope.refreshFields = function () {

View file

@ -22,6 +22,8 @@ define(function (require) {
// only link if the dataSource isn't already linked
vis.dataSource.$scope($scope);
}
vis.dataSource.fetch();
}
};
}

View file

@ -9,8 +9,10 @@ define(function (require) {
location: 'Visualization'
});
function Vis(config, state) {
config = config || {};
function Vis(opts) {
opts = opts || {};
var config = opts.config || {};
var state = opts.state || null;
// the visualization type
this.type = config.type || 'histogram';

View file

@ -118,7 +118,8 @@ define(function (require) {
var rendering = false;
return function renderRows(rows) {
[].push.apply(queue, rows);
// overwrite the queue, don't keep old rows
queue = rows.slice(0);
if (!rendering) {
onTick();
}

View file

@ -34,6 +34,7 @@ define(function (require) {
});
configFile.apps.forEach(function (app) {
if (app.id === 'discover') return;
$routeProvider.when('/' + app.id, {
templateUrl: 'kibana/apps/' + app.id + '/index.html'
});

View file

@ -1,43 +0,0 @@
define(function (require) {
var module = require('modules').get('kibana/services');
module.service('savedSearches', function (courier, configFile, $q) {
this.get = function (id) {
var docLoaded = id ? false : true;
var doc = courier.createSource('doc')
.index(configFile.kibanaIndex)
.type('saved_searches')
.id(id)
.on('results', function (doc) {
search.set(doc._source.state);
// set the
id = doc._id;
if (!docLoaded) {
docLoaded = true;
search.enable();
}
});
var search = courier.createSource('search');
search.save = function () {
var defer = $q.defer();
doc.doIndex({
state: search.toJSON()
}, function (err, id) {
if (err) return defer.reject(err);
defer.resolve();
});
return defer.promise;
};
if (!docLoaded) search.disableFetch();
return search;
};
this.create = this.get;
});
});