Merge pull request #15 from spenceralger/discover_crud

On the road to setting up the discover skelly
This commit is contained in:
spenceralger 2014-02-27 14:35:30 -07:00
commit 61eb549b59
15 changed files with 306 additions and 134 deletions

View file

@ -24,6 +24,12 @@ define(function (require) {
this._state = state;
this._courier = courier;
// before newListener to prevent unnecessary "emit" when added
this.on('removeListener', function onRemoveListener() {
if (EventEmitter.listenerCount(this, 'results') > 0) return;
courier._closeDataSource(this);
});
this.on('newListener', function (name, listener) {
if (name !== 'results') return;
@ -43,11 +49,6 @@ define(function (require) {
}
});
this.on('removeListener', function onRemoveListener() {
if (EventEmitter.listenerCount(this, 'results') > 0) return;
courier._closeDataSource(this);
});
this.extend = function () {
return courier
.createSource(this._getType())

View file

@ -54,11 +54,11 @@ define(function (require) {
});
cacheFieldsToObject(dataSource, fields);
callback(err, fields);
callback(err, self.getFieldsFromObject(dataSource));
});
} else {
cacheFieldsToObject(dataSource, fields);
callback(err, fields);
callback(err, self.getFieldsFromObject(dataSource));
}
});
}
@ -141,8 +141,15 @@ define(function (require) {
_.each(index.mappings, function (type) {
_.each(type, function (field, name) {
if (_.size(field.mapping) === 0 || name[0] === '_') return;
if (!_.isUndefined(fields[name]) && fields[name].type !== field.mapping[_.keys(field.mapping)[0]].type)
var mapping = field.mapping[_.keys(field.mapping)[0]];
mapping.type = castMappingType(mapping.type);
if (fields[name]) {
if (fields[name].type === mapping.type) return;
return courier._error(new Error.MappingConflict(name));
}
fields[name] = field.mapping[_.keys(field.mapping)[0]];
});
});
@ -181,6 +188,33 @@ define(function (require) {
return !_.isUndefined(mappings[dataSource._state.index]) ? true : false;
};
/**
* Accepts a mapping type, and converts it into it's js equivilent
* @param {String} type - the type from the mapping's 'type' field
* @return {String} - the most specific type that we care for
*/
var castMappingType = function (type) {
switch (type) {
case 'float':
case 'double':
case 'integer':
case 'long':
case 'short':
case 'byte':
case 'token_count':
return 'number';
case 'date':
case 'boolean':
case 'ip':
case 'attachment':
case 'geo_point':
case 'geo_shape':
return type;
default: // including 'string'
return 'string';
}
};
/**
* Clears mapping caches from elasticsearch and from local object
* @param {dataSource} dataSource

View file

@ -0,0 +1,15 @@
<ul>
<li ng-repeat="field in fields">
<span ng-click="toggle(field.name)">
<i
class="fa"
ng-class="{
'fa-check-square': !field.hidden,
'fa-square-o': field.hidden
}">
</i>
{{field.name}}
</span>
</li>
</ul>
<small class="pull-right"><a ng-click="refresh()">refresh field list</a></small>

View file

@ -0,0 +1,16 @@
define(function (require) {
var app = require('angular').module('app/discover');
var html = require('text!./field_chooser.html');
app.directive('discFieldChooser', function () {
return {
restrict: 'E',
scope: {
fields: '=',
toggle: '=',
refresh: '='
},
template: html
};
});
});

View file

@ -1,43 +1,39 @@
<div ng-controller="discover">
<h1>Discover</h1>
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-sm-2">Index</label>
<div class="col-sm-10">
<input class="form-control" ng-model="index">
</div>
</div>
<!-- <div class="form-group">
<label class="control-label col-sm-3">Repeat Interval</label>
<div class="col-sm-9">
<select
<nav class="navbar navbar-default navbar-static-top subnav">
<form class="navbar-form navbar-left form-inline" role="search" ng-submit="fetch()">
<label class="control-label">Index</label>
<input class="form-control" ng-model="index">
<label class="control-label" for="size">Query</label>
<input type="text" class="form-control" ng-model="query" placeholder="search">
<label class="control-label" for="size">Limit</label>
<select
class="form-control"
ng-model="interval"
ng-options="i.display for i in intervalOptions">
name="size"
ng-model="size"
ng-options="size.display for size in sizeOptions">
</select>
</div>
</div> -->
<form class="form-group" ng-submit="reset()">
<label class="control-label col-sm-2">Query</label>
<div class="col-sm-10">
<div class="input-group">
<input class="form-control" ng-model="query" >
<span class="input-group-btn">
<button type="button" class="btn" ng-click="reset()">
<i class="glyphicon glyphicon-search"></i>
</button>
</span>
</div>
</div>
<label class="control-label" for="sort">Sort</label>
<select
class="form-control"
name="sort"
ng-model="sort"
ng-options="field.name for field in fields">
</select>
<button type="submit" class="btn btn-default">
<i class="fa fa-search"></i>
</button>
</form>
</nav>
<div class="row">
<div class="col-md-10">
<kbn-table rows="rows" columns="columns"></kbn-table>
</div>
<div class="col-md-2">
<disc-field-chooser
fields="fields"
toggle="toggleField"
refresh="refreshFieldList">
</disc-field-chooser>
</div>
</div>
<div class="input-group">
<label class="control-label col-sm-2">Limit</label>
<select
class="form-control"
ng-model="size"
ng-options="size.display for size in sizeOptions">
</select>
</div>
<kbn-table rows="rows" columns="columns"></kbn-table>
</div>

View file

@ -1,11 +1,12 @@
define(function (require) {
define(function (require, module, exports) {
var angular = require('angular');
var _ = require('lodash');
require('directives/table');
require('css!./styles/main.css');
require('./field_chooser');
require('services/saved_searches');
var app = angular.module('app/discover', []);
var app = angular.module('app/discover');
var sizeOptions = [
{ display: '30', val: 30 },
@ -13,7 +14,7 @@ define(function (require) {
{ display: '80', val: 80 },
{ display: '125', val: 125 },
{ display: '250', val: 250 },
{ display: 'Unlimited', val: null },
{ display: '500', val: 500 }
];
var intervalOptions = [
@ -25,20 +26,19 @@ define(function (require) {
{ display: 'Yearly', val: 'yearly' }
];
app.controller('discover', function ($scope, courier, config) {
var source = courier.rootSearchSource.extend()
.size(30)
.$scope($scope)
.on('results', function (res) {
if (!$scope.fields) getFields();
$scope.rows = res.hits.hits;
});
app.controller('discover', function ($scope, config, $q, $routeParams, savedSearches, courier) {
var source;
if ($routeParams.id) {
source = savedSearches.get($routeParams.id);
} else {
source = savedSearches.create();
}
// stores the complete list of fields
$scope.fields = [];
$scope.fields = null;
// stores the fields we want to fetch
$scope.columns = [];
$scope.columns = null;
// At what interval are your index patterns
$scope.intervalOptions = intervalOptions;
@ -48,56 +48,128 @@ define(function (require) {
$scope.sizeOptions = sizeOptions;
$scope.size = $scope.sizeOptions[0];
// watch the discover.defaultIndex config value for changes
// the index that will be
config.$watch('discover.defaultIndex', function (val) {
if (!val) {
config.set('discover.defaultIndex', '_all');
return;
if (!val) return config.set('discover.defaultIndex', '_all');
if (!$scope.index) {
$scope.index = val;
$scope.fetch();
}
// only set if datasource doesn't have an index
if (!source.get('index')) $scope.index = val;
});
$scope.$watch('index', function (val) {
// set the index on the data source
source.index(val);
// clear the columns and fields, then refetch when we so a search
$scope.columns = $scope.fields = null;
});
source
.size(30)
.$scope($scope)
.inherits(courier.rootSearchSource)
.on('results', function (res) {
if (!$scope.fields) getFields();
$scope.rows = res.hits.hits;
});
$scope.$watch('query', function (query) {
if (query) {
source.query({
$scope.fetch = function () {
if (!$scope.fields) getFields();
source
.size($scope.size.val)
.query(!$scope.query ? null : {
query_string: {
query: query
query: $scope.query
}
});
} else {
// clear the query
source.query(null);
}
});
$scope.$watch('size', function (selectedSize) {
source.size(selectedSize.val);
});
$scope.reset = function () {
// the check happens only when the results come in; prevents a race condition
// if (!$scope.fields) getFields();
courier.abort();
courier.fetch();
};
function getFields() {
source.getFields(function (err, fields) {
$scope.fields = fields;
$scope.columns = _.keys(fields);
source.source({
})
.source(!$scope.columns ? null : {
include: $scope.columns
});
if ($scope.sort) {
var sort = {};
sort[$scope.sort.name] = 'asc';
source.sort(sort);
}
if ($scope.index !== source.get('index')) {
// set the index on the data source
source.index($scope.index);
// clear the columns and fields, then refetch when we so a search
$scope.columns = $scope.fields = null;
}
// fetch just this datasource
source.fetch();
};
var activeGetFields;
function getFields() {
var defer = $q.defer();
if (!source.get('index')) {
// Without an index there is nothing to do here.
defer.resolve();
return defer.promise;
}
if (activeGetFields) {
activeGetFields.then(function () {
defer.resolve();
});
return;
}
var currentState = _.transform($scope.fields || [], function (current, field) {
current[field.name] = {
hidden: field.hidden
};
}, {});
source
.getFields()
.then(function (fields) {
$scope.fields = [];
$scope.columns = [];
_(fields)
.keys()
.sort()
.each(function (name) {
var field = fields[name];
field.name = name;
_.defaults(field, currentState[name]);
if (!field.hidden) $scope.columns.push(name);
$scope.fields.push(field);
});
defer.resolve();
}, defer.reject);
return defer.promise.then(function () {
activeGetFields = null;
});
}
$scope.toggleField = function (name) {
var field = _.find($scope.fields, { name: name });
// toggle the hidden property
field.hidden = !field.hidden;
// collect column names for non-hidden fields and sort
$scope.columns = _.transform($scope.fields, function (cols, field) {
if (!field.hidden) cols.push(field.name);
}, []).sort();
// if we are just removing a field, no reason to refetch
if (!field.hidden) {
$scope.fetch();
}
};
$scope.refreshFieldList = function () {
source.clearFieldCache(function () {
getFields(function () {
$scope.fetch();
});
});
};
$scope.$emit('application.load');
});
});

View file

@ -9,15 +9,11 @@ define(function (require) {
angular
.module('kibana/controllers')
.controller('kibana', function ($scope, courier, configFile) {
setTimeout(function () {
courier.start();
}, 15);
$scope.apps = configFile.apps;
$scope.activeApp = '';
$scope.$on('$routeChangeSuccess', function () {
if (courier.running()) courier.fetch();
$scope.$on('application.load', function () {
courier.start();
});
});
});

View file

@ -14,12 +14,6 @@ define(function (require) {
* <kbn-table results="queryResult"></kbn-table>
* ```
*/
var defaults = {
columns: [],
rows: []
};
module.directive('kbnTable', function () {
return {
restrict: 'E',
@ -28,23 +22,28 @@ define(function (require) {
columns: '=',
rows: '='
},
controller: function ($scope) {
_.defaults($scope, defaults);
link: function (scope, element, attrs) {
scope.$watch('rows', render);
scope.$watch('columns', render);
$scope.makeRowHtml = function (row) {
var html = '<tr>';
_.each($scope.columns, function (col) {
html += '<td>';
if (row[col] !== void 0) {
html += row[col];
} else {
html += row._source[col];
}
html += '</td>';
function render() {
var $body = element.find('tbody').empty();
if (!scope.rows || scope.rows.length === 0) return;
if (!scope.columns || scope.columns.length === 0) return;
_.each(scope.rows, function (row) {
var tr = document.createElement('tr');
_.each(scope.columns, function (name) {
var td = document.createElement('td');
td.innerText = row._source[name] || row[name] || '';
tr.appendChild(td);
});
$body.append(tr);
});
html += '</tr>';
return html;
};
}
}
};
});

View file

@ -29,8 +29,9 @@ define(function (require) {
return 'app/' + app.id;
}));
// create empty modules for all of the kibana and app modules
requiredAgularModules.forEach(function (name) {
if (name.indexOf('kibana/') === 0) angular.module(name, []);
if (/^(kibana|app)\//.test(name)) angular.module(name, []);
});
kibana.requires = requiredAgularModules;

View file

@ -1,12 +1,6 @@
<table class="table">
<thead>
<th ng-repeat="col in columns">{{col}}</th>
<th ng-repeat="name in columns">{{name}}</th>
</thead>
<tbody>
<tr ng-repeat="row in rows">
<td ng-repeat="col in columns">
{{row._source[col] || row[col]}}
</td>
</tr>
</tbody>
<tbody></tbody>
</table>

View file

@ -1,6 +1,7 @@
define(function (require) {
var angular = require('angular');
var Courier = require('courier/courier');
var DataSource = require('courier/data_source/data_source');
var DocSource = require('courier/data_source/doc');
var errors = require('courier/errors');
var configFile = require('../../config');
@ -13,6 +14,10 @@ define(function (require) {
.service('courier', function (es, $rootScope, promises) {
if (courier) return courier;
promises.playNice(DataSource.prototype, [
'getFields'
]);
promises.playNice(DocSource.prototype, [
'doUpdate',
'doIndex'

View file

@ -0,0 +1,43 @@
define(function (require) {
var module = require('angular').module('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;
});
});