diff --git a/docs/model.html b/docs/model.html index 61b73e8f..2a21273c 100644 --- a/docs/model.html +++ b/docs/model.html @@ -94,36 +94,79 @@ also returned.
A single entry or row in the dataset
my.Document = Backbone.Model.extend({
- __type__: 'Document'
-});my.DocumentList = Backbone.Collection.extend({
+ __type__: 'Document',
+ initialize: function() {
+ _.bindAll(this, 'getFieldValue');
+ },For the provided Field get the corresponding rendered computed data value +for this document.
getFieldValue: function(field) {
+ var val = this.get(field.id);
+ if (field.deriver) {
+ val = field.deriver(val, field, this);
+ }
+ if (field.renderer) {
+ val = field.renderer(val, field, this);
+ }
+ return val;
+ }
+});my.DocumentList = Backbone.Collection.extend({
__type__: 'DocumentList',
model: my.Document
-});Following attributes as standard:
+Following (Backbone) attributes as standard:
my.Field = Backbone.Model.extend({
- defaults: {
- id: null,
+Following additional instance properties:
+ +@property {Function} renderer: a function to render the data for this field. +Signature: function(value, field, doc) where value is the value of this +cell, field is corresponding field object and document is the document +object. Note that implementing functions can ignore arguments (e.g. +function(value) would be a valid formatter function).
+ +@property {Function} deriver: a function to derive/compute the value of data +in this field as a function of this field's value (if any) and the current +document, its signature and behaviour is the same as for renderer. Use of +this function allows you to define an entirely new value for data in this +field. This provides support for a) 'derived/computed' fields: i.e. fields +whose data are functions of the data in other fields b) transforming the +value of this field prior to rendering.
my.Field = Backbone.Model.extend({ defaults: {
label: null,
- type: 'String'
- },
- initialize: function(data) {if a hash not passed in the first argument throw error
if ('0' in data) {
+ type: 'string',
+ format: null,
+ is_derived: false
+ },@param {Object} data: standard Backbone model attributes
+ +@param {Object} options: renderer and/or deriver functions.
initialize: function(data, options) {if a hash not passed in the first argument throw error
if ('0' in data) {
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
}
if (this.attributes.label == null) {
this.set({label: this.id});
}
+ if (options) {
+ this.renderer = options.renderer;
+ this.deriver = options.deriver;
+ }
}
});
my.FieldList = Backbone.Collection.extend({
model: my.Field
-});Query instances encapsulate a query to the backend (see query method on backend). Useful both @@ -179,10 +222,10 @@ execution.
return { size: 100 , from: 0 - , facets: {}http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html -, filter: {}
list of simple filters which will be add to 'add' filter of filter
, filters: []
+ , facets: {}http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html +, filter: {}
, filters: []
}
- },Set (update or add) a terms filter to filters
@@ -191,13 +234,23 @@ execution. var filter = { term: {} }; filter.term[fieldId] = value; filters.push(filter); - this.set({filters: filters});change does not seem to be triggered ...
this.trigger('change');
- },change does not seem to be triggered automatically
if (value) {
+ this.trigger('change');
+ } else {adding a new blank filter and do not want to trigger a new query
this.trigger('change:filters:new-blank');
+ }
+ },Remove a filter from filters at index filterIndex
removeFilter: function(filterIndex) {
+ var filters = this.get('filters');
+ filters.splice(filterIndex, 1);
+ this.set({filters: filters});
+ this.trigger('change');
+ },Add a Facet to this query
See http://www.elasticsearch.org/guide/reference/api/search/facets/
addFacet: function(fieldId) {
- var facets = this.get('facets');Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
if (_.contains(_.keys(facets), fieldId)) {
+ var facets = this.get('facets');Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
if (_.contains(_.keys(facets), fieldId)) {
return;
}
facets[fieldId] = {
@@ -206,7 +259,7 @@ execution.
this.set({facets: facets}, {silent: true});
this.trigger('facet:add', this);
}
-});Object to store Facet information, that is summary information (e.g. values and counts) about a field obtained by some faceting method on the @@ -253,9 +306,9 @@ key used to specify this facet in the facet query):
terms: [] } } -});my.FacetList = Backbone.Collection.extend({
+});my.FacetList = Backbone.Collection.extend({
model: my.Facet
-});Backends will register themselves by id into this registry
my.backends = {};
diff --git a/docs/view-flot-graph.html b/docs/view-flot-graph.html
index 7ef5b8eb..cc2893c3 100644
--- a/docs/view-flot-graph.html
+++ b/docs/view-flot-graph.html
@@ -1,4 +1,6 @@
- view-flot-graph.js Jump To … view-flot-graph.js
this.recline = this.recline || {};
+ view-flot-graph.js Jump To … view-flot-graph.js
/*jshint multistr:true */
+
+this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
Graph view for a Dataset using Flot graphing library.
diff --git a/docs/view-grid.html b/docs/view-grid.html
index eb67d2e1..9b058bdd 100644
--- a/docs/view-grid.html
+++ b/docs/view-grid.html
@@ -1,21 +1,17 @@
- view-grid.js Jump To … view-grid.js
this.recline = this.recline || {};
+ view-grid.js Jump To … view-grid.js
/*jshint multistr:true */
+
+this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
DataGrid
Provides a tabular view on a Dataset.
-Initialize it with a recline.Dataset object.
-
-Additional options passed in second arguments. Options:
-
-
-- cellRenderer: function used to render individual cells. See DataGridRow for more.
-
my.DataGrid = Backbone.View.extend({
+Initialize it with a recline.Model.Dataset.
my.DataGrid = Backbone.View.extend({
tagName: "div",
- className: "data-table-container",
+ className: "recline-grid-container",
- initialize: function(modelEtc, options) {
+ initialize: function(modelEtc) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
@@ -24,7 +20,6 @@
this.model.currentDocuments.bind('remove', this.render);
this.state = {};
this.hiddenFields = [];
- this.options = options;
},
events: {
@@ -67,6 +62,9 @@ Column and row menus
facet: function() {
self.model.queryState.addFacet(self.state.currentColumn);
},
+ filter: function() {
+ self.model.queryState.addTermFilter(self.state.currentColumn, '');
+ },
transform: function() { self.showTransformDialog('transform') },
sortAsc: function() { self.setColumnSort('asc') },
sortDesc: function() { self.setColumnSort('desc') },
@@ -135,7 +133,7 @@ from DOM) while id may be int }, ======================================================
Templating
template: ' \
- <table class="data-table table-striped table-condensed" cellspacing="0"> \
+ <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
<thead> \
<tr> \
{{#notEmpty}} \
@@ -154,9 +152,13 @@ from DOM) while id may be int
<a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
<ul class="dropdown-menu data-table-menu pull-right"> \
<li><a data-action="facet" href="JavaScript:void(0);">Facet on this Field</a></li> \
+ <li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \
+ <li class="divider"></li> \
<li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
<li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
+ <li class="divider"></li> \
<li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
+ <li class="divider"></li> \
<li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
</ul> \
</div> \
@@ -188,9 +190,7 @@ from DOM) while id may be int model: doc,
el: tr,
fields: self.fields
- },
- self.options
- );
+ });
newView.render();
});
this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
@@ -202,16 +202,6 @@ from DOM) while id may be int In addition you must pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
-Additional options can be passed in a second hash argument. Options:
-
-
-- cellRenderer: function to render cells. Signature: function(value,
-field, doc) where value is the value of this cell, field is
-corresponding field object and document is the document object. Note
-that implementing functions can ignore arguments (e.g.
-function(value) would be a valid cellRenderer function).
-
-
Example:
@@ -219,21 +209,11 @@ var row = new DataGridRow({
model: dataset-document,
el: dom-element,
fields: mydatasets.fields // a FieldList object
- }, {
- cellRenderer: my-cell-renderer-function
- }
-);
+ });
my.DataGridRow = Backbone.View.extend({
- initialize: function(initData, options) {
+ initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
- if (options && options.cellRenderer) {
- this._cellRenderer = options.cellRenderer;
- } else {
- this._cellRenderer = function(value) {
- return value;
- }
- }
this.el = $(this.el);
this.model.bind('change', this.render);
},
@@ -268,7 +248,7 @@ var row = new DataGridRow({
var cellData = this._fields.map(function(field) {
return {
field: field.id,
- value: self._cellRenderer(doc.get(field.id), field, doc)
+ value: doc.getFieldValue(field)
}
})
return { id: this.id, cells: cellData }
diff --git a/docs/view.html b/docs/view.html
index daf29400..0bebc221 100644
--- a/docs/view.html
+++ b/docs/view.html
@@ -1,4 +1,5 @@
- view.js Jump To … view.js
this.recline = this.recline || {};
+ view.js Jump To … view.js
/*jshint multistr:true */
+this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
DataExplorer
@@ -53,7 +54,7 @@ operate in read-only mode (hiding all editing options).
NB: the element already being in the DOM is important for rendering of
FlotGraph subview.
my.DataExplorer = Backbone.View.extend({
template: ' \
- <div class="data-explorer"> \
+ <div class="recline-data-explorer"> \
<div class="alert-messages"></div> \
\
<div class="header"> \
@@ -65,6 +66,12 @@ FlotGraph subview.
<div class="recline-results-info"> \
Results found <span class="doc-count">{{docCount}}</span> \
</div> \
+ <div class="menu-right"> \
+ <a href="#" class="btn" data-action="filters">Filters</a> \
+ <a href="#" class="btn" data-action="facets">Facets</a> \
+ </div> \
+ <div class="query-editor-here" style="display:inline;"></div> \
+ <div class="clearfix"></div> \
</div> \
<div class="data-view-container"></div> \
<div class="dialog-overlay" style="display: none; z-index: 101; "> </div> \
@@ -75,6 +82,9 @@ FlotGraph subview.
</div> \
</div> \
',
+ events: {
+ 'click .menu-right a': 'onMenuClick'
+ },
initialize: function(options) {
var self = this;
@@ -158,11 +168,17 @@ note this.model and dataset returned are the same
var queryEditor = new my.QueryEditor({
model: this.model.queryState
});
- this.el.find('.header').append(queryEditor.el);
- var queryFacetEditor = new my.FacetViewer({
+ this.el.find('.query-editor-here').append(queryEditor.el);
+ var filterEditor = new my.FilterEditor({
+ model: this.model.queryState
+ });
+ this.$filterEditor = filterEditor.el;
+ this.el.find('.header').append(filterEditor.el);
+ var facetViewer = new my.FacetViewer({
model: this.model
});
- this.el.find('.header').append(queryFacetEditor.el);
+ this.$facetViewer = facetViewer.el;
+ this.el.find('.header').append(facetViewer.el);
},
setupRouting: function() {
@@ -188,10 +204,19 @@ note this.model and dataset returned are the same
view.view.el.hide();
}
});
+ },
+
+ onMenuClick: function(e) {
+ e.preventDefault();
+ var action = $(e.target).attr('data-action');
+ if (action === 'filters') {
+ this.$filterEditor.show();
+ } else if (action === 'facets') {
+ this.$facetViewer.show();
+ }
}
});
-
my.QueryEditor = Backbone.View.extend({
className: 'recline-query-editor',
template: ' \
@@ -199,28 +224,21 @@ note this.model and dataset returned are the same
<div class="input-prepend text-query"> \
<span class="add-on"><i class="icon-search"></i></span> \
<input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
- <div class="btn-group menu"> \
- <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
- <ul class="dropdown-menu"> \
- <li><a data-action="size" href="">Number of items to show ({{size}})</a></li> \
- <li><a data-action="from" href="">Show from ({{from}})</a></li> \
- </ul> \
- </div> \
</div> \
<div class="pagination"> \
<ul> \
<li class="prev action-pagination-update"><a href="">«</a></li> \
- <li class="active"><a>{{from}} – {{to}}</a></li> \
+ <li class="active"><a><input name="from" type="text" value="{{from}}" /> – <input name="to" type="text" value="{{to}}" /> </a></li> \
<li class="next action-pagination-update"><a href="">»</a></li> \
</ul> \
</div> \
+ <button type="submit" class="btn">Go »</button> \
</form> \
',
events: {
'submit form': 'onFormSubmit'
, 'click .action-pagination-update': 'onPaginationUpdate'
- , 'click .menu li a': 'onMenuItemClick'
},
initialize: function() {
@@ -232,7 +250,9 @@ note this.model and dataset returned are the same
onFormSubmit: function(e) {
e.preventDefault();
var query = this.el.find('.text-query input').val();
- this.model.set({q: query});
+ var newFrom = parseInt(this.el.find('input[name="from"]').val());
+ var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
+ this.model.set({size: newSize, from: newFrom, q: query});
},
onPaginationUpdate: function(e) {
e.preventDefault();
@@ -244,20 +264,6 @@ note this.model and dataset returned are the same
}
this.model.set({from: newFrom});
},
- onMenuItemClick: function(e) {
- e.preventDefault();
- var attrName = $(e.target).attr('data-action');
- var msg = _.template('New value (<%= value %>)',
- {value: this.model.get(attrName)}
- );
- var newValue = prompt(msg);
- if (newValue) {
- newValue = parseInt(newValue);
- var update = {};
- update[attrName] = newValue;
- this.model.set(update);
- }
- },
render: function() {
var tmplData = this.model.toJSON();
tmplData.to = this.model.get('from') + this.model.get('size');
@@ -266,6 +272,101 @@ note this.model and dataset returned are the same
}
});
+my.FilterEditor = Backbone.View.extend({
+ className: 'recline-filter-editor well',
+ template: ' \
+ <a class="close js-hide" href="#">×</a> \
+ <div class="row filters"> \
+ <div class="span1"> \
+ <h3>Filters</h3> \
+ </div> \
+ <div class="span11"> \
+ <form class="form-horizontal"> \
+ <div class="row"> \
+ <div class="span6"> \
+ {{#termFilters}} \
+ <div class="control-group filter-term filter" data-filter-id={{id}}> \
+ <label class="control-label" for="">{{label}}</label> \
+ <div class="controls"> \
+ <div class="input-append"> \
+ <input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
+ <a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
+ </div> \
+ </div> \
+ </div> \
+ {{/termFilters}} \
+ </div> \
+ <div class="span4"> \
+ <p>To add a filter use the column menu in the grid view.</p> \
+ <button type="submit" class="btn">Update</button> \
+ </div> \
+ </form> \
+ </div> \
+ </div> \
+ ',
+ events: {
+ 'click .js-hide': 'onHide',
+ 'click .js-remove-filter': 'onRemoveFilter',
+ 'submit form': 'onTermFiltersUpdate'
+ },
+ initialize: function() {
+ this.el = $(this.el);
+ _.bindAll(this, 'render');
+ this.model.bind('change', this.render);
+ this.model.bind('change:filters:new-blank', this.render);
+ this.render();
+ },
+ render: function() {
+ var tmplData = $.extend(true, {}, this.model.toJSON()); we will use idx in list as there id ...
tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+ filter.id = idx;
+ return filter;
+ });
+ tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
+ return filter.term !== undefined;
+ });
+ tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
+ var fieldId = _.keys(filter.term)[0];
+ return {
+ id: filter.id,
+ fieldId: fieldId,
+ label: fieldId,
+ value: filter.term[fieldId]
+ }
+ });
+ var out = $.mustache(this.template, tmplData);
+ this.el.html(out);
are there actually any facets to show?
if (this.model.get('filters').length > 0) {
+ this.el.show();
+ } else {
+ this.el.hide();
+ }
+ },
+ onHide: function(e) {
+ e.preventDefault();
+ this.el.hide();
+ },
+ onRemoveFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ var filterId = $target.closest('.filter').attr('data-filter-id');
+ this.model.removeFilter(filterId);
+ },
+ onTermFiltersUpdate: function(e) {
+ var self = this;
+ e.preventDefault();
+ var filters = self.model.get('filters');
+ var $form = $(e.target);
+ _.each($form.find('input'), function(input) {
+ var $input = $(input);
+ var filterIndex = parseInt($input.attr('data-filter-id'));
+ var value = $input.val();
+ var fieldId = $input.attr('data-filter-field');
+ filters[filterIndex].term[fieldId] = value;
+ });
+ self.model.set({filters: filters});
+ self.model.trigger('change');
+ }
+});
+
my.FacetViewer = Backbone.View.extend({
className: 'recline-facet-viewer well',
template: ' \
@@ -279,7 +380,7 @@ note this.model and dataset returned are the same
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
<ul class="facet-items dropdown-menu"> \
{{#terms}} \
- <li><input type="checkbox" class="facet-choice js-facet-filter" value="{{term}}" name="{{term}}" /> <label for="{{term}}">{{term}} ({{count}})</label></li> \
+ <li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
{{/terms}} \
</ul> \
</div> \
@@ -289,7 +390,7 @@ note this.model and dataset returned are the same
events: {
'click .js-hide': 'onHide',
- 'change .js-facet-filter': 'onFacetFilter'
+ 'click .js-facet-filter': 'onFacetFilter'
},
initialize: function(model) {
_.bindAll(this, 'render');
@@ -304,7 +405,7 @@ note this.model and dataset returned are the same
fields: this.model.fields.toJSON()
};
var templated = $.mustache(this.template, tmplData);
- this.el.html(templated); are there actually any facets to show?
if (this.model.facets.length > 0) {
+ this.el.html(templated);
are there actually any facets to show?
if (this.model.facets.length > 0) {
this.el.show();
} else {
this.el.hide();
@@ -314,14 +415,15 @@ note this.model and dataset returned are the same
e.preventDefault();
this.el.hide();
},
- onFacetFilter: function(e) { todo: uncheck
var $checkbox = $(e.target);
- var fieldId = $checkbox.closest('.facet-summary').attr('data-facet');
- var value = $checkbox.val();
+ onFacetFilter: function(e) {
+ var $target= $(e.target);
+ var fieldId = $target.closest('.facet-summary').attr('data-facet');
+ var value = $target.attr('data-value');
this.model.queryState.addTermFilter(fieldId, value);
}
});
-/* ========================================================== */
Miscellaneous Utilities
var urlPathRegex = /^([^?]+)(\?.*)?/;
Parse the Hash section of a URL into path and query string
my.parseHashUrl = function(hashUrl) {
+/* ========================================================== */
Miscellaneous Utilities
var urlPathRegex = /^([^?]+)(\?.*)?/;
Parse the Hash section of a URL into path and query string
my.parseHashUrl = function(hashUrl) {
var parsed = urlPathRegex.exec(hashUrl);
if (parsed == null) {
return {};
@@ -331,7 +433,7 @@ note this.model and dataset returned are the same
query: parsed[2] || ''
}
}
-} Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
+}
Parse a URL query string (?xyz=abc...) into a dictionary.
my.parseQueryString = function(q) {
if (!q) {
return {};
}
@@ -344,13 +446,13 @@ note this.model and dataset returned are the same
if (q && q.length && q[0] === '?') {
q = q.slice(1);
}
- while (e = r.exec(q)) { TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
+ while (e = r.exec(q)) {
TODO: have values be array as query string allow repetition of keys
urlParams[d(e[1])] = d(e[2]);
}
return urlParams;
-}
Parse the query string out of the URL hash
my.parseHashQueryString = function() {
+}
Parse the query string out of the URL hash
my.parseHashQueryString = function() {
q = my.parseHashUrl(window.location.hash).query;
return my.parseQueryString(q);
-}
Compse a Query String
my.composeQueryString = function(queryParams) {
+}
Compse a Query String
my.composeQueryString = function(queryParams) {
var queryString = '?';
var items = [];
$.each(queryParams, function(key, value) {
@@ -362,7 +464,7 @@ note this.model and dataset returned are the same
my.getNewHashForQueryString = function(queryParams) {
var queryPart = my.composeQueryString(queryParams);
- if (window.location.hash) { slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
+ if (window.location.hash) {
slice(1) to remove # at start
return window.location.hash.split('?')[0].slice(1) + queryPart;
} else {
return queryPart;
}
@@ -370,7 +472,7 @@ note this.model and dataset returned are the same
my.setHashQueryString = function(queryParams) {
window.location.hash = my.getNewHashForQueryString(queryParams);
-} notify
+} notify
Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are:
@@ -393,7 +495,7 @@ note this.model and dataset returned are the same
{{/loader}} \
</div>';
var _templated = $.mustache(_template, tmplData);
- _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
+ _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!options.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
@@ -401,10 +503,10 @@ note this.model and dataset returned are the same
});
}, 1000);
}
-} clearNotifications
+} clearNotifications
Clear all existing notifications
my.clearNotifications = function() {
- var $notifications = $('.data-explorer .alert-messages .alert');
+ var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.remove();
}
diff --git a/recline.js b/recline.js
index 73860da4..11c3f779 100644
--- a/recline.js
+++ b/recline.js
@@ -72,17 +72,33 @@ this.recline.Model = this.recline.Model || {};
(function($, my) {
-// ## A Dataset model
+// ## A Dataset model
//
// A model has the following (non-Backbone) attributes:
//
-// * fields: (aka columns) is a FieldList listing all the fields on this
-// Dataset (this can be set explicitly, or, will be set by Dataset.fetch() or Dataset.query()
-// * currentDocuments: a DocumentList containing the Documents we have
-// currently loaded for viewing (you update currentDocuments by calling query)
-// * docCount: total number of documents in this dataset
+// @property {FieldList} fields: (aka columns) is a `FieldList` listing all the
+// fields on this Dataset (this can be set explicitly, or, will be set by
+// Dataset.fetch() or Dataset.query()
+//
+// @property {DocumentList} currentDocuments: a `DocumentList` containing the
+// Documents we have currently loaded for viewing (updated by calling query
+// method)
+//
+// @property {number} docCount: total number of documents in this dataset
+//
+// @property {Backend} backend: the Backend (instance) for this Dataset
+//
+// @property {Query} queryState: `Query` object which stores current
+// queryState. queryState may be edited by other components (e.g. a query
+// editor view) changes will trigger a Dataset query.
+//
+// @property {FacetList} facets: FacetList object containing all current
+// Facets.
my.Dataset = Backbone.Model.extend({
__type__: 'Dataset',
+ // ### initialize
+ //
+ // Sets up instance properties (see above)
initialize: function(model, backend) {
_.bindAll(this, 'query');
this.backend = backend;
@@ -154,11 +170,29 @@ my.Dataset = Backbone.Model.extend({
}
});
-// ## A Document (aka Row)
+// ## A Document (aka Row)
//
// A single entry or row in the dataset
my.Document = Backbone.Model.extend({
- __type__: 'Document'
+ __type__: 'Document',
+ initialize: function() {
+ _.bindAll(this, 'getFieldValue');
+ },
+
+ // ### getFieldValue
+ //
+ // For the provided Field get the corresponding rendered computed data value
+ // for this document.
+ getFieldValue: function(field) {
+ var val = this.get(field.id);
+ if (field.deriver) {
+ val = field.deriver(val, field, this);
+ }
+ if (field.renderer) {
+ val = field.renderer(val, field, this);
+ }
+ return val;
+ }
});
// ## A Backbone collection of Documents
@@ -167,29 +201,59 @@ my.DocumentList = Backbone.Collection.extend({
model: my.Document
});
-// ## A Field (aka Column) on a Dataset
+// ## A Field (aka Column) on a Dataset
//
-// Following attributes as standard:
+// Following (Backbone) attributes as standard:
//
-// * id: a unique identifer for this field- usually this should match the key in the documents hash
-// * label: the visible label used for this field
-// * type: the type of the data
+// * id: a unique identifer for this field- usually this should match the key in the documents hash
+// * label: (optional: defaults to id) the visible label used for this field
+// * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on
+// * format: (optional) used to indicate how the data should be formatted. For example:
+// * type=date, format=yyyy-mm-dd
+// * type=float, format=percentage
+// * type=float, format='###,###.##'
+// * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
+//
+// Following additional instance properties:
+//
+// @property {Function} renderer: a function to render the data for this field.
+// Signature: function(value, field, doc) where value is the value of this
+// cell, field is corresponding field object and document is the document
+// object. Note that implementing functions can ignore arguments (e.g.
+// function(value) would be a valid formatter function).
+//
+// @property {Function} deriver: a function to derive/compute the value of data
+// in this field as a function of this field's value (if any) and the current
+// document, its signature and behaviour is the same as for renderer. Use of
+// this function allows you to define an entirely new value for data in this
+// field. This provides support for a) 'derived/computed' fields: i.e. fields
+// whose data are functions of the data in other fields b) transforming the
+// value of this field prior to rendering.
my.Field = Backbone.Model.extend({
+ // ### defaults - define default values
defaults: {
- id: null,
label: null,
- type: 'String'
+ type: 'string',
+ format: null,
+ is_derived: false
},
- // In addition to normal backbone initialization via a Hash you can also
- // just pass a single argument representing id to the ctor
- initialize: function(data) {
- // if a hash not passed in the first argument is set as value for key 0
+ // ### initialize
+ //
+ // @param {Object} data: standard Backbone model attributes
+ //
+ // @param {Object} options: renderer and/or deriver functions.
+ initialize: function(data, options) {
+ // if a hash not passed in the first argument throw error
if ('0' in data) {
throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
}
if (this.attributes.label == null) {
this.set({label: this.id});
}
+ if (options) {
+ this.renderer = options.renderer;
+ this.deriver = options.deriver;
+ }
}
});
@@ -197,30 +261,99 @@ my.FieldList = Backbone.Collection.extend({
model: my.Field
});
-// ## A Query object storing Dataset Query state
+// ## Query
+//
+// Query instances encapsulate a query to the backend (see query method on backend). Useful both
+// for creating queries and for storing and manipulating query state -
+// e.g. from a query editor).
+//
+// **Query Structure and format**
+//
+// Query structure should follow that of [ElasticSearch query
+// language](http://www.elasticsearch.org/guide/reference/api/search/).
+//
+// **NB: It is up to specific backends how to implement and support this query
+// structure. Different backends might choose to implement things differently
+// or not support certain features. Please check your backend for details.**
+//
+// Query object has the following key attributes:
+//
+// * size (=limit): number of results to return
+// * from (=offset): offset into result set - http://www.elasticsearch.org/guide/reference/api/search/from-size.html
+// * sort: sort order -
+// * query: Query in ES Query DSL
+// * filter: See filters and Filtered Query
+// * fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html
+// * facets: TODO - see http://www.elasticsearch.org/guide/reference/api/search/facets/
+//
+// Additions:
+//
+// * q: either straight text or a hash will map directly onto a [query_string
+// query](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
+// in backend
+//
+// * Of course this can be re-interpreted by different backends. E.g. some
+// may just pass this straight through e.g. for an SQL backend this could be
+// the full SQL query
+//
+// * filters: dict of ElasticSearch filters. These will be and-ed together for
+// execution.
+//
+// **Examples**
+//
+//
+// {
+// q: 'quick brown fox',
+// filters: [
+// { term: { 'owner': 'jones' } }
+// ]
+// }
+//
my.Query = Backbone.Model.extend({
defaults: function() {
return {
size: 100
, from: 0
, facets: {}
- // http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html
+ //
// , filter: {}
- // list of simple filters which will be add to 'add' filter of filter
, filters: []
}
},
- // Set (update or add) a terms filter
- // http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html
+ // #### addTermFilter
+ //
+ // Set (update or add) a terms filter to filters
+ //
+ // See
addTermFilter: function(fieldId, value) {
var filters = this.get('filters');
var filter = { term: {} };
filter.term[fieldId] = value;
filters.push(filter);
this.set({filters: filters});
- // change does not seem to be triggered ...
+ // change does not seem to be triggered automatically
+ if (value) {
+ this.trigger('change');
+ } else {
+ // adding a new blank filter and do not want to trigger a new query
+ this.trigger('change:filters:new-blank');
+ }
+ },
+ // ### removeFilter
+ //
+ // Remove a filter from filters at index filterIndex
+ removeFilter: function(filterIndex) {
+ var filters = this.get('filters');
+ filters.splice(filterIndex, 1);
+ this.set({filters: filters});
this.trigger('change');
},
+ // ### addFacet
+ //
+ // Add a Facet to this query
+ //
+ // See
addFacet: function(fieldId) {
var facets = this.get('facets');
// Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
@@ -236,18 +369,51 @@ my.Query = Backbone.Model.extend({
});
-// ## A Facet (Result)
+// ## A Facet (Result)
+//
+// Object to store Facet information, that is summary information (e.g. values
+// and counts) about a field obtained by some faceting method on the
+// backend.
+//
+// Structure of a facet follows that of Facet results in ElasticSearch, see:
+//
+//
+// Specifically the object structure of a facet looks like (there is one
+// addition compared to ElasticSearch: the "id" field which corresponds to the
+// key used to specify this facet in the facet query):
+//
+//
+// {
+// "id": "id-of-facet",
+// // type of this facet (terms, range, histogram etc)
+// "_type" : "terms",
+// // total number of tokens in the facet
+// "total": 5,
+// // @property {number} number of documents which have no value for the field
+// "missing" : 0,
+// // number of facet values not included in the returned facets
+// "other": 0,
+// // term object ({term: , count: ...})
+// "terms" : [ {
+// "term" : "foo",
+// "count" : 2
+// }, {
+// "term" : "bar",
+// "count" : 2
+// }, {
+// "term" : "baz",
+// "count" : 1
+// }
+// ]
+// }
+//
my.Facet = Backbone.Model.extend({
defaults: function() {
return {
_type: 'terms',
- // total number of tokens in the facet
total: 0,
- // number of facet values not included in the returned facets
other: 0,
- // number of documents which have no value for the field
missing: 0,
- // term object ({term: , count: ...})
terms: []
}
}
@@ -265,6 +431,8 @@ my.backends = {};
}(jQuery, this.recline.Model));
+/*jshint multistr:true */
+
var util = function() {
var templates = {
transformActions: 'Global transform... '
@@ -415,6 +583,8 @@ var util = function() {
observeExit: observeExit
};
}();
+/*jshint multistr:true */
+
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -767,6 +937,8 @@ my.FlotGraph = Backbone.View.extend({
})(jQuery, recline.View);
+/*jshint multistr:true */
+
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -775,16 +947,12 @@ this.recline.View = this.recline.View || {};
//
// Provides a tabular view on a Dataset.
//
-// Initialize it with a recline.Dataset object.
-//
-// Additional options passed in second arguments. Options:
-//
-// * cellRenderer: function used to render individual cells. See DataGridRow for more.
+// Initialize it with a `recline.Model.Dataset`.
my.DataGrid = Backbone.View.extend({
tagName: "div",
- className: "data-table-container",
+ className: "recline-grid-container",
- initialize: function(modelEtc, options) {
+ initialize: function(modelEtc) {
var self = this;
this.el = $(this.el);
_.bindAll(this, 'render');
@@ -793,7 +961,6 @@ my.DataGrid = Backbone.View.extend({
this.model.currentDocuments.bind('remove', this.render);
this.state = {};
this.hiddenFields = [];
- this.options = options;
},
events: {
@@ -843,6 +1010,9 @@ my.DataGrid = Backbone.View.extend({
facet: function() {
self.model.queryState.addFacet(self.state.currentColumn);
},
+ filter: function() {
+ self.model.queryState.addTermFilter(self.state.currentColumn, '');
+ },
transform: function() { self.showTransformDialog('transform') },
sortAsc: function() { self.setColumnSort('asc') },
sortDesc: function() { self.setColumnSort('desc') },
@@ -915,7 +1085,7 @@ my.DataGrid = Backbone.View.extend({
// ======================================================
// #### Templating
template: ' \
- \
+ \
\
\
{{#notEmpty}} \
@@ -934,9 +1104,13 @@ my.DataGrid = Backbone.View.extend({
\
\
\
@@ -970,9 +1144,7 @@ my.DataGrid = Backbone.View.extend({
model: doc,
el: tr,
fields: self.fields
- },
- self.options
- );
+ });
newView.render();
});
this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
@@ -986,14 +1158,6 @@ my.DataGrid = Backbone.View.extend({
//
// In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
//
-// Additional options can be passed in a second hash argument. Options:
-//
-// * cellRenderer: function to render cells. Signature: function(value,
-// field, doc) where value is the value of this cell, field is
-// corresponding field object and document is the document object. Note
-// that implementing functions can ignore arguments (e.g.
-// function(value) would be a valid cellRenderer function).
-//
// Example:
//
//
@@ -1001,22 +1165,12 @@ my.DataGrid = Backbone.View.extend({
// model: dataset-document,
// el: dom-element,
// fields: mydatasets.fields // a FieldList object
-// }, {
-// cellRenderer: my-cell-renderer-function
-// }
-// );
+// });
//
my.DataGridRow = Backbone.View.extend({
- initialize: function(initData, options) {
+ initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
- if (options && options.cellRenderer) {
- this._cellRenderer = options.cellRenderer;
- } else {
- this._cellRenderer = function(value) {
- return value;
- }
- }
this.el = $(this.el);
this.model.bind('change', this.render);
},
@@ -1051,7 +1205,7 @@ my.DataGridRow = Backbone.View.extend({
var cellData = this._fields.map(function(field) {
return {
field: field.id,
- value: self._cellRenderer(doc.get(field.id), field, doc)
+ value: doc.getFieldValue(field)
}
})
return { id: this.id, cells: cellData }
@@ -1104,6 +1258,174 @@ my.DataGridRow = Backbone.View.extend({
});
})(jQuery, recline.View);
+/*jshint multistr:true */
+
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+
+my.Map = Backbone.View.extend({
+
+ tagName: 'div',
+ className: 'data-map-container',
+
+ latitudeFieldNames: ['lat','latitude'],
+ longitudeFieldNames: ['lon','longitude'],
+ geometryFieldNames: ['geom','the_geom','geometry','spatial'],
+
+ //TODO: In case we want to change the default markers
+ /*
+ markerOptions: {
+ radius: 5,
+ color: 'grey',
+ fillColor: 'orange',
+ weight: 2,
+ opacity: 1,
+ fillOpacity: 1
+ },
+ */
+
+ template: ' \
+ \
+ \
+',
+
+ initialize: function(options, config) {
+ var self = this;
+
+ this.el = $(this.el);
+ this.render();
+ this.model.bind('change', function() {
+ self._setupGeometryField();
+ });
+
+ this.mapReady = false;
+ },
+
+ render: function() {
+
+ var self = this;
+
+ htmls = $.mustache(this.template, this.model.toTemplateJSON());
+ $(this.el).html(htmls);
+ this.$map = this.el.find('.panel.map');
+
+ this.model.bind('query:done', function() {
+ if (!self.geomReady){
+ self._setupGeometryField();
+ }
+
+ if (!self.mapReady){
+ self._setupMap();
+ }
+ self.redraw()
+ });
+
+ return this;
+ },
+
+ redraw: function(){
+
+ var self = this;
+
+ if (this.geomReady){
+ if (this.model.currentDocuments.length > 0){
+ this.features.clearLayers();
+ var bounds = new L.LatLngBounds();
+
+ this.model.currentDocuments.forEach(function(doc){
+ var feature = self._getGeometryFromDocument(doc);
+ if (feature){
+ // Build popup contents
+ // TODO: mustache?
+ html = ''
+ for (key in doc.attributes){
+ html += '' + key + ': '+ doc.attributes[key] + ''
+ }
+ feature.properties = {popupContent: html};
+
+ self.features.addGeoJSON(feature);
+
+ // TODO: bounds and center map
+ }
+ });
+ }
+ }
+ },
+
+ _getGeometryFromDocument: function(doc){
+ if (this.geomReady){
+ if (this._geomFieldName){
+ // We assume that the contents of the field are a valid GeoJSON object
+ return doc.attributes[this._geomFieldName];
+ } else if (this._lonFieldName && this._latFieldName){
+ // We'll create a GeoJSON like point object from the two lat/lon fields
+ return {
+ type: 'Point',
+ coordinates: [
+ doc.attributes[this._lonFieldName],
+ doc.attributes[this._latFieldName],
+ ]
+ }
+ }
+ return null;
+ }
+ },
+
+ _setupGeometryField: function(){
+ var geomField, latField, lonField;
+
+ // Check if there is a field with GeoJSON geometries or alternatively,
+ // two fields with lat/lon values
+ this._geomFieldName = this._checkField(this.geometryFieldNames);
+ this._latFieldName = this._checkField(this.latitudeFieldNames);
+ this._lonFieldName = this._checkField(this.longitudeFieldNames);
+
+ // TODO: Allow users to choose the fields
+
+ this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+ },
+
+ _checkField: function(fieldNames){
+ var field;
+ for (var i = 0; i < fieldNames.length; i++){
+ field = this.model.fields.get(fieldNames[i]);
+ if (field) return field.id;
+ }
+ return null;
+ },
+
+ _setupMap: function(){
+
+ this.map = new L.Map(this.$map.get(0));
+
+ // MapQuest OpenStreetMap base map
+ var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+ var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of MapQuest
';
+ var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
+ this.map.addLayer(bg);
+
+ // Layer to hold the features
+ this.features = new L.GeoJSON();
+ this.features.on('featureparse', function (e) {
+ if (e.properties && e.properties.popupContent){
+ e.layer.bindPopup(e.properties.popupContent);
+ }
+ });
+ this.map.addLayer(this.features);
+
+ this.map.setView(new L.LatLng(0, 0), 2);
+
+ this.mapReady = true;
+ }
+
+ });
+
+})(jQuery, recline.View);
+
+/*jshint multistr:true */
+
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -1310,6 +1632,7 @@ my.ColumnTransform = Backbone.View.extend({
});
})(jQuery, recline.View);
+/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
@@ -1365,7 +1688,7 @@ this.recline.View = this.recline.View || {};
// FlotGraph subview.
my.DataExplorer = Backbone.View.extend({
template: ' \
- \
+ \
\
\
\
@@ -1377,6 +1700,12 @@ my.DataExplorer = Backbone.View.extend({
\
Results found {{docCount}} \
\
+ \
+ \
+ \
\
\
\
@@ -1387,6 +1716,9 @@ my.DataExplorer = Backbone.View.extend({
\
\
',
+ events: {
+ 'click .menu-right a': 'onMenuClick'
+ },
initialize: function(options) {
var self = this;
@@ -1479,11 +1811,17 @@ my.DataExplorer = Backbone.View.extend({
var queryEditor = new my.QueryEditor({
model: this.model.queryState
});
- this.el.find('.header').append(queryEditor.el);
- var queryFacetEditor = new my.FacetViewer({
+ this.el.find('.query-editor-here').append(queryEditor.el);
+ var filterEditor = new my.FilterEditor({
+ model: this.model.queryState
+ });
+ this.$filterEditor = filterEditor.el;
+ this.el.find('.header').append(filterEditor.el);
+ var facetViewer = new my.FacetViewer({
model: this.model
});
- this.el.find('.header').append(queryFacetEditor.el);
+ this.$facetViewer = facetViewer.el;
+ this.el.find('.header').append(facetViewer.el);
},
setupRouting: function() {
@@ -1513,10 +1851,19 @@ my.DataExplorer = Backbone.View.extend({
view.view.el.hide();
}
});
+ },
+
+ onMenuClick: function(e) {
+ e.preventDefault();
+ var action = $(e.target).attr('data-action');
+ if (action === 'filters') {
+ this.$filterEditor.show();
+ } else if (action === 'facets') {
+ this.$facetViewer.show();
+ }
}
});
-
my.QueryEditor = Backbone.View.extend({
className: 'recline-query-editor',
template: ' \
@@ -1524,28 +1871,21 @@ my.QueryEditor = Backbone.View.extend({
\
\
\
- \
\
\
\
- «
\
- - {{from}} – {{to}}
\
+ - –
\
- »
\
\
\
+ \
\
',
events: {
'submit form': 'onFormSubmit'
, 'click .action-pagination-update': 'onPaginationUpdate'
- , 'click .menu li a': 'onMenuItemClick'
},
initialize: function() {
@@ -1557,7 +1897,9 @@ my.QueryEditor = Backbone.View.extend({
onFormSubmit: function(e) {
e.preventDefault();
var query = this.el.find('.text-query input').val();
- this.model.set({q: query});
+ var newFrom = parseInt(this.el.find('input[name="from"]').val());
+ var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
+ this.model.set({size: newSize, from: newFrom, q: query});
},
onPaginationUpdate: function(e) {
e.preventDefault();
@@ -1569,20 +1911,6 @@ my.QueryEditor = Backbone.View.extend({
}
this.model.set({from: newFrom});
},
- onMenuItemClick: function(e) {
- e.preventDefault();
- var attrName = $(e.target).attr('data-action');
- var msg = _.template('New value (<%= value %>)',
- {value: this.model.get(attrName)}
- );
- var newValue = prompt(msg);
- if (newValue) {
- newValue = parseInt(newValue);
- var update = {};
- update[attrName] = newValue;
- this.model.set(update);
- }
- },
render: function() {
var tmplData = this.model.toJSON();
tmplData.to = this.model.get('from') + this.model.get('size');
@@ -1591,6 +1919,105 @@ my.QueryEditor = Backbone.View.extend({
}
});
+my.FilterEditor = Backbone.View.extend({
+ className: 'recline-filter-editor well',
+ template: ' \
+ × \
+ \
+ \
+ Filters
\
+ \
+ \
+ \
+ ',
+ events: {
+ 'click .js-hide': 'onHide',
+ 'click .js-remove-filter': 'onRemoveFilter',
+ 'submit form': 'onTermFiltersUpdate'
+ },
+ initialize: function() {
+ this.el = $(this.el);
+ _.bindAll(this, 'render');
+ this.model.bind('change', this.render);
+ this.model.bind('change:filters:new-blank', this.render);
+ this.render();
+ },
+ render: function() {
+ var tmplData = $.extend(true, {}, this.model.toJSON());
+ // we will use idx in list as there id ...
+ tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+ filter.id = idx;
+ return filter;
+ });
+ tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
+ return filter.term !== undefined;
+ });
+ tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
+ var fieldId = _.keys(filter.term)[0];
+ return {
+ id: filter.id,
+ fieldId: fieldId,
+ label: fieldId,
+ value: filter.term[fieldId]
+ }
+ });
+ var out = $.mustache(this.template, tmplData);
+ this.el.html(out);
+ // are there actually any facets to show?
+ if (this.model.get('filters').length > 0) {
+ this.el.show();
+ } else {
+ this.el.hide();
+ }
+ },
+ onHide: function(e) {
+ e.preventDefault();
+ this.el.hide();
+ },
+ onRemoveFilter: function(e) {
+ e.preventDefault();
+ var $target = $(e.target);
+ var filterId = $target.closest('.filter').attr('data-filter-id');
+ this.model.removeFilter(filterId);
+ },
+ onTermFiltersUpdate: function(e) {
+ var self = this;
+ e.preventDefault();
+ var filters = self.model.get('filters');
+ var $form = $(e.target);
+ _.each($form.find('input'), function(input) {
+ var $input = $(input);
+ var filterIndex = parseInt($input.attr('data-filter-id'));
+ var value = $input.val();
+ var fieldId = $input.attr('data-filter-field');
+ filters[filterIndex].term[fieldId] = value;
+ });
+ self.model.set({filters: filters});
+ self.model.trigger('change');
+ }
+});
+
my.FacetViewer = Backbone.View.extend({
className: 'recline-facet-viewer well',
template: ' \
@@ -1604,7 +2031,7 @@ my.FacetViewer = Backbone.View.extend({
{{id}} {{label}} \
\
\
@@ -1614,7 +2041,7 @@ my.FacetViewer = Backbone.View.extend({
events: {
'click .js-hide': 'onHide',
- 'change .js-facet-filter': 'onFacetFilter'
+ 'click .js-facet-filter': 'onFacetFilter'
},
initialize: function(model) {
_.bindAll(this, 'render');
@@ -1642,10 +2069,9 @@ my.FacetViewer = Backbone.View.extend({
this.el.hide();
},
onFacetFilter: function(e) {
- // todo: uncheck
- var $checkbox = $(e.target);
- var fieldId = $checkbox.closest('.facet-summary').attr('data-facet');
- var value = $checkbox.val();
+ var $target= $(e.target);
+ var fieldId = $target.closest('.facet-summary').attr('data-facet');
+ var value = $target.attr('data-value');
this.model.queryState.addTermFilter(fieldId, value);
}
});
@@ -1742,7 +2168,7 @@ my.notify = function(message, options) {
{{/loader}} \
';
var _templated = $.mustache(_template, tmplData);
- _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
+ _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
if (!options.persist) {
setTimeout(function() {
$(_templated).fadeOut(1000, function() {
@@ -1756,7 +2182,7 @@ my.notify = function(message, options) {
//
// Clear all existing notifications
my.clearNotifications = function() {
- var $notifications = $('.data-explorer .alert-messages .alert');
+ var $notifications = $('.recline-data-explorer .alert-messages .alert');
$notifications.remove();
}