Discovery Panel Skeleton

- renamed calculateIndices files to be calculate_indices
 - hooked up dataSource.getFields() to the mapper, uses source._wrapcb() to properly trigger digest cycles
 - fixed an error handling bug in the search source
 - courier error DocFetchFailure is now just FetchFailure and will be sent from SearchSources as well
 - courier/index_pattern module is now gone
 - fixed a bug in the mapper that improperly triggered errors when a pattern matched multiple indices that had similar fields
 - prototype for discover app is included
 - directives/kbn_view module is now directives/view, the kbn prefix is implied
 - added basic table directive
This commit is contained in:
Spencer Alger 2014-02-26 10:09:26 -07:00
parent 661d89f741
commit 6cb518b035
22 changed files with 338 additions and 87 deletions

View file

@ -24,7 +24,11 @@ define(function (require) {
// execute a search right now
search: function (courier) {
if (courier._activeSearchRequest) {
return courier._error(new HastyRefresh());
// ensure that this happens async, otherwise listeners
// might miss error events
return nextTick(function () {
courier._error(new HastyRefresh());
});
}
courier._activeSearchRequest = SearchSource.fetch(
@ -174,19 +178,24 @@ define(function (require) {
}, this);
};
// be default, the courier will throw an error if a fetch
// occurs before a previous fetch finishes. To prevent this, you
// should call abort before calling .fetch()
Courier.prototype.abort = function () {
if (this._activeSearchRequest) {
this._activeSearchRequest.abort();
this._activeSearchRequest = null;
}
};
// force a fetch of all datasources right now, optionally filter by type
Courier.prototype.fetch = function (onlyType) {
var courier = this;
// ensure that onFetch functions always run after the tick
// so that users can will be able to listen after or before the call to
// fetch and always get the same behavior (even if the onFetch runs synchronously)
nextTick(function () {
_.forOwn(onFetch, function (fn, type) {
if (onlyType && onlyType !== type) return;
if (courier._refs[type].length) fn(courier);
courier._refs[type].forEach(function (ref) {
ref.fetchCount ++;
});
_.forOwn(onFetch, function (fn, type) {
if (onlyType && onlyType !== type) return;
if (courier._refs[type].length) fn(courier);
courier._refs[type].forEach(function (ref) {
ref.fetchCount ++;
});
});
};

View file

@ -3,7 +3,6 @@ define(function (require) {
var _ = require('lodash');
var EventEmitter = require('utils/event_emitter');
var Mapper = require('courier/mapper');
var IndexPattern = require('courier/index_pattern');
function DataSource(courier, initialState) {
var state;
@ -54,9 +53,6 @@ define(function (require) {
this._methods.forEach(function (name) {
this[name] = function (val) {
state[name] = val;
if (name === 'index' && arguments[1]) {
state.index = new IndexPattern(val, arguments[1]);
}
return this;
};
}, this);
@ -81,8 +77,8 @@ define(function (require) {
* @callback {Error, Array} - calls cb with a possible error or an array of field names
* @todo
*/
DataSource.prototype.getFieldNames = function (cb) {
throw new Error('not implemented');
DataSource.prototype.getFields = function (cb) {
this._courier._mapper.getFields(this, this._wrapcb(cb));
};
/**
@ -114,32 +110,33 @@ define(function (require) {
* @return {this} - chainable
*/
DataSource.prototype.$scope = function ($scope) {
var emitter = this;
var courier = this;
if (emitter._emitter$scope) {
emitter._emitter$scope = $scope;
if (courier._$scope) {
// simply change the scope that callbacks will point to
courier._$scope = $scope;
return this;
}
emitter._emitter$scope = $scope;
var origOn = emitter.on;
courier._$scope = $scope;
emitter.on = function (event, listener) {
var wrapped = function () {
var args = arguments;
// always use the stored ref so that it can be updated if needed
var $scope = emitter._emitter$scope;
$scope[$scope.$$phase ? '$eval' : '$apply'](function () {
listener.apply(emitter, args);
});
};
// wrap the 'on' method so that all listeners
// can be wrapped in calls to $scope.$apply
var origOn = courier.on;
courier.on = function (event, listener) {
var wrapped = courier._wrapcb(listener);
// set .listener so that it can be removed by
// .removeListener() using the original function
wrapped.listener = listener;
return origOn.call(emitter, event, wrapped);
return origOn.call(courier, event, wrapped);
};
emitter.on.restore = function () {
delete emitter._emitter$scope;
emitter.on = origOn;
// make sure the alias is still set
courier.addListener = courier.on;
courier.on.restore = function () {
delete courier._$scope;
courier.on = courier.addListener = origOn;
};
return this;
@ -213,11 +210,22 @@ define(function (require) {
return flatState;
};
DataSource.prototype._resolveIndexPattern = function (start, end) {
if (this._state.indexInterval) {
throw new Error('Not implemented');
}
return this._state.index;
DataSource.prototype._wrapcb = function (cb) {
var courier = this;
var wrapped = function () {
var args = arguments;
// always use the stored ref so that it can be updated if needed
var $scope = courier._$scope;
// don't fall apart if we don't have a scope
if (!$scope) return cb.apply(courier, args);
// use angular's $apply or $eval functions for the given scope
$scope[$scope.$$phase ? '$eval' : '$apply'](function () {
cb.apply(courier, args);
});
};
return wrapped;
};
return DataSource;

View file

@ -2,7 +2,8 @@ define(function (require) {
var DataSource = require('courier/data_source/data_source');
var inherits = require('utils/inherits');
var nextTick = require('utils/next_tick');
var errors = require('courier/errors');
var VersionConflict = require('courier/errors').VersionConflict;
var FetchFailure = require('courier/errors').FetchFailure;
var listenerCount = require('utils/event_emitter').listenerCount;
var _ = require('lodash');
@ -42,7 +43,7 @@ define(function (require) {
var ref = allRefs[i];
var source = ref.source;
if (resp.error) return source._error(new errors.DocFetchFailure(resp));
if (resp.error) return source._error(new FetchFailure(resp));
if (resp.found) {
if (ref.version === resp._version) return; // no change
ref.version = resp._version;
@ -147,7 +148,7 @@ define(function (require) {
if (err) return cb(err);
if (resp && resp.status === 409) {
err = new errors.VersionConflict(resp);
err = new VersionConflict(resp);
if (listenerCount(source, 'conflict')) {
return source.emit('conflict', err);
} else {

View file

@ -2,6 +2,7 @@ define(function (require) {
var DataSource = require('courier/data_source/data_source');
var inherits = require('utils/inherits');
var errors = require('courier/errors');
var FetchFailure = require('courier/errors').FetchFailure;
var _ = require('lodash');
function SearchSource(courier, initialState) {
@ -44,7 +45,7 @@ define(function (require) {
_.each(resp.responses, function (resp, i) {
var source = allRefs[i];
if (resp.error) return errors.emit(source, courier, resp);
if (resp.error) return source._error(new FetchFailure(resp));
source.emit('results', resp);
});

View file

@ -38,17 +38,17 @@ define(function (require) {
/**
* DocFetchFailure Error - where there is an error getting a doc
* FetchFailure Error - where there is an error getting a doc
* @param {String} [msg] - An error message that will probably end up in a log.
*/
errors.DocFetchFailure = function DocFetchFailure(resp) {
errors.FetchFailure = function FetchFailure(resp) {
CourierError.call(this,
'Failed to get the doc: ' + JSON.stringify(resp),
errors.DocFetchFailure);
errors.FetchFailure);
this.resp = resp;
};
inherits(errors.DocFetchFailure, CourierError);
inherits(errors.FetchFailure, CourierError);
/**

View file

@ -1,23 +0,0 @@
define(function (require) {
function IndexPattern(index, interval) {
this.text = index;
this.interval = interval;
}
IndexPattern.prototype = {
toJSON: function () {
return this.text;
},
toString: function () {
return this.text;
},
forTimeRange: function () {
throw new Error('not implemented');
},
wildcard: function () {
throw new Error('not implemented');
}
};
return IndexPattern;
});

View file

@ -133,17 +133,15 @@ define(function (require) {
// TODO: Add week/month check
client.indices.getFieldMapping(params, function (err, response, status) {
// TODO: Add error message
var fields = {};
_.each(response, function (index) {
_.each(response, function (index, indexName) {
if (indexName === config.cacheIndex) return;
_.each(index.mappings, function (type) {
_.each(type, function (field, name) {
if (_.isUndefined(field.mapping) || name[0] === '_') return;
if (!_.isUndefined(fields[name]) && fields[name] !== field.mapping[_.keys(field.mapping)[0]])
return courier._error(new Error.MappingConflict());
if (_.size(field.mapping) === 0 || name[0] === '_') return;
if (!_.isUndefined(fields[name]) && fields[name].type !== field.mapping[_.keys(field.mapping)[0]].type)
return courier._error(new Error.MappingConflict(name));
fields[name] = field.mapping[_.keys(field.mapping)[0]];
});
});

View file

@ -1 +1,43 @@
<h1>Discover</h1>
<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
class="form-control"
ng-model="interval"
ng-options="i.display for i in intervalOptions">
</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>
</form>
</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,6 +1,103 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
require('directives/table');
require('css!./styles/main.css');
var app = angular.module('app/discover', []);
var sizeOptions = [
{ display: '30', val: 30 },
{ display: '50', val: 50 },
{ display: '80', val: 80 },
{ display: '125', val: 125 },
{ display: '250', val: 250 },
{ display: 'Unlimited', val: null },
];
var intervalOptions = [
{ 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, courier, config) {
var source = courier.rootSearchSource.extend()
.size(30)
.$scope($scope)
.on('results', function (res) {
if (!$scope.fields) getFields();
$scope.rows = res.hits.hits;
});
// stores the complete list of fields
$scope.fields = [];
// stores the fields we want to fetch
$scope.columns = [];
// At what interval are your index patterns
$scope.intervalOptions = intervalOptions;
$scope.interval = $scope.intervalOptions[0];
// options to control the size of the queries
$scope.sizeOptions = sizeOptions;
$scope.size = $scope.sizeOptions[0];
// watch the discover.defaultIndex config value for changes
config.$watch('discover.defaultIndex', function (val) {
if (!val) {
config.set('discover.defaultIndex', '_all');
return;
}
// 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;
});
$scope.$watch('query', function (query) {
if (query) {
source.query({
query_string: {
query: 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({
include: $scope.columns
});
});
}
});
});

View file

@ -0,0 +1,51 @@
define(function (require) {
var html = require('text!partials/table.html');
var angular = require('angular');
var _ = require('lodash');
var module = angular.module('kibana/directives');
/**
* kbnTable directive
*
* displays results in a simple table view. Pass the result object
* via the results attribute on the kbnTable element:
* ```
* <kbn-table results="queryResult"></kbn-table>
* ```
*/
var defaults = {
columns: [],
rows: []
};
module.directive('kbnTable', function () {
return {
restrict: 'E',
template: html,
scope: {
columns: '=',
rows: '='
},
controller: function ($scope) {
_.defaults($scope, defaults);
$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>';
});
html += '</tr>';
return html;
};
}
};
});
});

View file

@ -77,7 +77,7 @@ define(function (require) {
// require global modules
require([
'controllers/kibana',
'directives/kbn_view',
'directives/view',
'constants/base'
], loaded());

View file

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

View file

@ -7,6 +7,7 @@ require.config({
'angular-route': '../bower_components/angular-route/angular-route',
async: '../bower_components/async/lib/async',
css: '../bower_components/require-css/css',
text: '../bower_components/requirejs-text/text',
d3: '../bower_components/d3/d3',
elasticsearch: '../bower_components/elasticsearch/elasticsearch.angular',
jquery: '../bower_components/jquery/jquery',

View file

@ -0,0 +1,55 @@
// Core variables and mixins
@import "../../bower_components/bootstrap/less/variables.less";
@icon-font-path: "../../bower_components/bootstrap/fonts/";
@import "../../bower_components/bootstrap/less/mixins.less";
// Reset
@import "../../bower_components/bootstrap/less/normalize.less";
@import "../../bower_components/bootstrap/less/print.less";
// Core CSS
@import "../../bower_components/bootstrap/less/scaffolding.less";
@import "../../bower_components/bootstrap/less/type.less";
@import "../../bower_components/bootstrap/less/code.less";
@import "../../bower_components/bootstrap/less/grid.less";
@import "../../bower_components/bootstrap/less/tables.less";
@import "../../bower_components/bootstrap/less/forms.less";
@import "../../bower_components/bootstrap/less/buttons.less";
// Components
@import "../../bower_components/bootstrap/less/component-animations.less";
@import "../../bower_components/bootstrap/less/glyphicons.less";
@import "../../bower_components/bootstrap/less/dropdowns.less";
@import "../../bower_components/bootstrap/less/button-groups.less";
@import "../../bower_components/bootstrap/less/input-groups.less";
@import "../../bower_components/bootstrap/less/navs.less";
@import "../../bower_components/bootstrap/less/navbar.less";
@import "../../bower_components/bootstrap/less/breadcrumbs.less";
@import "../../bower_components/bootstrap/less/pagination.less";
@import "../../bower_components/bootstrap/less/pager.less";
@import "../../bower_components/bootstrap/less/labels.less";
@import "../../bower_components/bootstrap/less/badges.less";
@import "../../bower_components/bootstrap/less/jumbotron.less";
@import "../../bower_components/bootstrap/less/thumbnails.less";
@import "../../bower_components/bootstrap/less/alerts.less";
@import "../../bower_components/bootstrap/less/progress-bars.less";
@import "../../bower_components/bootstrap/less/media.less";
@import "../../bower_components/bootstrap/less/list-group.less";
@import "../../bower_components/bootstrap/less/panels.less";
@import "../../bower_components/bootstrap/less/wells.less";
@import "../../bower_components/bootstrap/less/close.less";
// Components w/ JavaScript
@import "../../bower_components/bootstrap/less/modals.less";
@import "../../bower_components/bootstrap/less/tooltip.less";
@import "../../bower_components/bootstrap/less/popovers.less";
@import "../../bower_components/bootstrap/less/carousel.less";
// Utility classes
@import "../../bower_components/bootstrap/less/utilities.less";
@import "../../bower_components/bootstrap/less/responsive-utilities.less";
// automatically include the theme file
@import "../../bower_components/bootstrap/less/theme.less";

View file

@ -2367,8 +2367,8 @@ input[type="button"].btn-block {
}
@font-face {
font-family: 'Glyphicons Halflings';
src: url('../fonts/glyphicons-halflings-regular.eot');
src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
src: url('../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot');
src: url('../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
.glyphicon {
position: relative;

View file

@ -1,6 +1,5 @@
@import "../../bower_components/bootstrap/less/bootstrap.less";
@import "../../bower_components/bootstrap/less/theme.less";
@import "./_bootstrap.less";
body {
margin: 60px 10px;
}
}

View file

@ -3,7 +3,7 @@ module.exports = {
src: [
'<%= app %>/styles/**/*.less',
'<%= app %>/apps/**/*.less',
'!_*.less'
'!<%= src %>/**/_*.less'
],
expand: true,
ext: '.css',

View file

@ -14,7 +14,7 @@
}
}
});
require(["/specs/calculateIndices.js","/specs/courier.js","/specs/data_source.js","/specs/mapper.js"], function () {
require(["/specs/calculate_indices.js","/specs/courier.js","/specs/data_source.js","/specs/mapper.js"], function () {
window.mochaRunner = mocha.run().on('end', function () {
window.mochaResults = this.stats;
});