[merge] from upstream master
This commit is contained in:
@@ -12,7 +12,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// Override Backbone.sync to hand off to sync function in relevant backend
|
||||
Backbone.sync = function(method, model, options) {
|
||||
return model.backend.sync(method, model, options);
|
||||
}
|
||||
};
|
||||
|
||||
// ## recline.Backend.Base
|
||||
//
|
||||
|
||||
@@ -38,14 +38,14 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
var self = this;
|
||||
var base = this.get('dataproxy_url');
|
||||
var data = {
|
||||
url: dataset.get('url')
|
||||
, 'max-results': queryObj.size
|
||||
, type: dataset.get('format')
|
||||
url: dataset.get('url'),
|
||||
'max-results': queryObj.size,
|
||||
type: dataset.get('format')
|
||||
};
|
||||
var jqxhr = $.ajax({
|
||||
url: base
|
||||
, data: data
|
||||
, dataType: 'jsonp'
|
||||
url: base,
|
||||
data: data,
|
||||
dataType: 'jsonp'
|
||||
});
|
||||
var dfd = $.Deferred();
|
||||
this._wrapInTimeout(jqxhr).done(function(results) {
|
||||
|
||||
@@ -59,37 +59,33 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
}
|
||||
},
|
||||
_normalizeQuery: function(queryObj) {
|
||||
if (queryObj.toJSON) {
|
||||
var out = queryObj.toJSON();
|
||||
} else {
|
||||
var out = _.extend({}, queryObj);
|
||||
}
|
||||
if (out.q != undefined && out.q.trim() === '') {
|
||||
var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
|
||||
if (out.q !== undefined && out.q.trim() === '') {
|
||||
delete out.q;
|
||||
}
|
||||
if (!out.q) {
|
||||
out.query = {
|
||||
match_all: {}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
out.query = {
|
||||
query_string: {
|
||||
query: out.q
|
||||
}
|
||||
}
|
||||
};
|
||||
delete out.q;
|
||||
}
|
||||
// now do filters (note the *plural*)
|
||||
if (out.filters && out.filters.length) {
|
||||
if (!out.filter) {
|
||||
out.filter = {}
|
||||
out.filter = {};
|
||||
}
|
||||
if (!out.filter.and) {
|
||||
out.filter.and = [];
|
||||
}
|
||||
out.filter.and = out.filter.and.concat(out.filters);
|
||||
}
|
||||
if (out.filters != undefined) {
|
||||
if (out.filters !== undefined) {
|
||||
delete out.filters;
|
||||
}
|
||||
return out;
|
||||
@@ -107,10 +103,10 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// TODO: fail case
|
||||
jqxhr.done(function(results) {
|
||||
_.each(results.hits.hits, function(hit) {
|
||||
if (!'id' in hit._source && hit._id) {
|
||||
if (!('id' in hit._source) && hit._id) {
|
||||
hit._source.id = hit._id;
|
||||
}
|
||||
})
|
||||
});
|
||||
if (results.facets) {
|
||||
results.hits.facets = results.facets;
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
return url;
|
||||
} else {
|
||||
// https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0
|
||||
var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/
|
||||
var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
|
||||
var matches = url.match(regex);
|
||||
if (matches) {
|
||||
var key = matches[1];
|
||||
var worksheet = 1;
|
||||
var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
|
||||
var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
|
||||
return out;
|
||||
} else {
|
||||
alert('Failed to extract gdocs key from ' + url);
|
||||
@@ -52,8 +52,9 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// cache data onto dataset (we have loaded whole gdoc it seems!)
|
||||
model._dataCache = result.data;
|
||||
dfd.resolve(model);
|
||||
})
|
||||
return dfd.promise(); }
|
||||
});
|
||||
return dfd.promise();
|
||||
}
|
||||
},
|
||||
|
||||
query: function(dataset, queryObj) {
|
||||
@@ -64,7 +65,9 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// TODO: factor this out as a common method with other backends
|
||||
var objs = _.map(dataset._dataCache, function (d) {
|
||||
var obj = {};
|
||||
_.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
|
||||
_.each(_.zip(fields, d), function (x) {
|
||||
obj[x[0]] = x[1];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
dfd.resolve(this._docsToQueryResult(objs));
|
||||
@@ -101,8 +104,8 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
if (gdocsSpreadsheet.feed.entry.length > 0) {
|
||||
for (var k in gdocsSpreadsheet.feed.entry[0]) {
|
||||
if (k.substr(0, 3) == 'gsx') {
|
||||
var col = k.substr(4)
|
||||
results.field.push(col);
|
||||
var col = k.substr(4);
|
||||
results.field.push(col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
};
|
||||
reader.onerror = function (e) {
|
||||
alert('Failed to load file. Code: ' + e.target.error.code);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
});
|
||||
var dataset = recline.Backend.createDataset(data, fields);
|
||||
return dataset;
|
||||
}
|
||||
};
|
||||
|
||||
// Converts a Comma Separated Values string into an array of arrays.
|
||||
// Each line in the CSV becomes an array.
|
||||
|
||||
@@ -15,7 +15,7 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
// If not defined (or id not provided) id will be autogenerated.
|
||||
my.createDataset = function(data, fields, metadata) {
|
||||
if (!metadata) {
|
||||
var metadata = {};
|
||||
metadata = {};
|
||||
}
|
||||
if (!metadata.id) {
|
||||
metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
|
||||
@@ -78,8 +78,8 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
},
|
||||
sync: function(method, model, options) {
|
||||
var self = this;
|
||||
var dfd = $.Deferred();
|
||||
if (method === "read") {
|
||||
var dfd = $.Deferred();
|
||||
if (model.__type__ == 'Dataset') {
|
||||
var rawDataset = this.datasets[model.id];
|
||||
model.set(rawDataset.metadata);
|
||||
@@ -89,7 +89,6 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'update') {
|
||||
var dfd = $.Deferred();
|
||||
if (model.__type__ == 'Document') {
|
||||
_.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
|
||||
if(doc.id === model.id) {
|
||||
@@ -100,7 +99,6 @@ this.recline.Backend = this.recline.Backend || {};
|
||||
}
|
||||
return dfd.promise();
|
||||
} else if (method === 'delete') {
|
||||
var dfd = $.Deferred();
|
||||
if (model.__type__ == 'Document') {
|
||||
var rawDataset = self.datasets[model.dataset.id];
|
||||
var newdocs = _.reject(rawDataset.documents, function(doc) {
|
||||
|
||||
107
src/model.js
107
src/model.js
@@ -106,7 +106,25 @@ my.Dataset = Backbone.Model.extend({
|
||||
//
|
||||
// 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
|
||||
@@ -117,25 +135,71 @@ my.DocumentList = Backbone.Collection.extend({
|
||||
|
||||
// ## <a id="field">A Field (aka Column) on a Dataset</a>
|
||||
//
|
||||
// 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 <http://www.elasticsearch.org/guide/reference/mapping/>
|
||||
// * 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
|
||||
},
|
||||
initialize: function(data) {
|
||||
// ### 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) {
|
||||
if (this.attributes.label === null) {
|
||||
this.set({label: this.id});
|
||||
}
|
||||
if (options) {
|
||||
this.renderer = options.renderer;
|
||||
this.deriver = options.deriver;
|
||||
}
|
||||
if (!this.renderer) {
|
||||
this.renderer = this.defaultRenderers[this.get('type')];
|
||||
}
|
||||
},
|
||||
defaultRenderers: {
|
||||
object: function(val, field, doc) {
|
||||
return JSON.stringify(val);
|
||||
},
|
||||
'float': function(val, field, doc) {
|
||||
var format = field.get('format');
|
||||
if (format === 'percentage') {
|
||||
return val + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,7 +231,7 @@ my.FieldList = Backbone.Collection.extend({
|
||||
// * query: Query in ES Query DSL <http://www.elasticsearch.org/guide/reference/api/search/query.html>
|
||||
// * filter: See filters and <a href="http://www.elasticsearch.org/guide/reference/query-dsl/filtered-query.html">Filtered Query</a>
|
||||
// * 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/
|
||||
// * facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/
|
||||
//
|
||||
// Additions:
|
||||
//
|
||||
@@ -195,13 +259,13 @@ my.FieldList = Backbone.Collection.extend({
|
||||
my.Query = Backbone.Model.extend({
|
||||
defaults: function() {
|
||||
return {
|
||||
size: 100
|
||||
, from: 0
|
||||
, facets: {}
|
||||
size: 100,
|
||||
from: 0,
|
||||
facets: {},
|
||||
// <http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html>
|
||||
// , filter: {}
|
||||
, filters: []
|
||||
}
|
||||
filters: []
|
||||
};
|
||||
},
|
||||
// #### addTermFilter
|
||||
//
|
||||
@@ -247,6 +311,17 @@ my.Query = Backbone.Model.extend({
|
||||
};
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:add', this);
|
||||
},
|
||||
addHistogramFacet: function(fieldId) {
|
||||
var facets = this.get('facets');
|
||||
facets[fieldId] = {
|
||||
date_histogram: {
|
||||
field: fieldId,
|
||||
interval: 'day'
|
||||
}
|
||||
};
|
||||
this.set({facets: facets}, {silent: true});
|
||||
this.trigger('facet:add', this);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -297,7 +372,7 @@ my.Facet = Backbone.Model.extend({
|
||||
other: 0,
|
||||
missing: 0,
|
||||
terms: []
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
17
src/util.js
17
src/util.js
@@ -2,8 +2,8 @@
|
||||
|
||||
var util = function() {
|
||||
var templates = {
|
||||
transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>'
|
||||
, cellEditor: ' \
|
||||
transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>',
|
||||
cellEditor: ' \
|
||||
<div class="menu-container data-table-cell-editor"> \
|
||||
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
|
||||
<div id="data-table-cell-editor-actions"> \
|
||||
@@ -13,8 +13,8 @@ var util = function() {
|
||||
</div> \
|
||||
</div> \
|
||||
</div> \
|
||||
'
|
||||
, editPreview: ' \
|
||||
',
|
||||
editPreview: ' \
|
||||
<div class="expression-preview-table-wrapper"> \
|
||||
<table> \
|
||||
<thead> \
|
||||
@@ -63,7 +63,7 @@ var util = function() {
|
||||
function registerEmitter() {
|
||||
var Emitter = function(obj) {
|
||||
this.emit = function(obj, channel) {
|
||||
if (!channel) var channel = 'data';
|
||||
if (!channel) channel = 'data';
|
||||
this.trigger(channel, obj);
|
||||
};
|
||||
};
|
||||
@@ -80,7 +80,7 @@ var util = function() {
|
||||
104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/",
|
||||
112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8",
|
||||
120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
|
||||
}
|
||||
};
|
||||
window.addEventListener("keyup", function(e) {
|
||||
var pressed = shortcuts[e.keyCode];
|
||||
if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed);
|
||||
@@ -126,10 +126,11 @@ var util = function() {
|
||||
if ( !options ) options = {data: {}};
|
||||
if ( !options.data ) options = {data: options};
|
||||
var html = $.mustache( templates[template], options.data );
|
||||
var targetDom = null;
|
||||
if (target instanceof jQuery) {
|
||||
var targetDom = target;
|
||||
targetDom = target;
|
||||
} else {
|
||||
var targetDom = $( "." + target + ":first" );
|
||||
targetDom = $( "." + target + ":first" );
|
||||
}
|
||||
if( options.append ) {
|
||||
targetDom.append( html );
|
||||
|
||||
@@ -80,10 +80,10 @@ my.FlotGraph = Backbone.View.extend({
|
||||
',
|
||||
|
||||
events: {
|
||||
'change form select': 'onEditorSubmit'
|
||||
, 'click .editor-add': 'addSeries'
|
||||
, 'click .action-remove-series': 'removeSeries'
|
||||
, 'click .action-toggle-help': 'toggleHelp'
|
||||
'change form select': 'onEditorSubmit',
|
||||
'click .editor-add': 'addSeries',
|
||||
'click .action-remove-series': 'removeSeries',
|
||||
'click .action-toggle-help': 'toggleHelp'
|
||||
},
|
||||
|
||||
initialize: function(options, config) {
|
||||
@@ -129,12 +129,12 @@ my.FlotGraph = Backbone.View.extend({
|
||||
var series = this.$series.map(function () {
|
||||
return $(this).val();
|
||||
});
|
||||
this.chartConfig.series = $.makeArray(series)
|
||||
this.chartConfig.series = $.makeArray(series);
|
||||
this.chartConfig.group = this.el.find('.editor-group select').val();
|
||||
this.chartConfig.graphType = this.el.find('.editor-type select').val();
|
||||
// update navigation
|
||||
var qs = my.parseHashQueryString();
|
||||
qs['graph'] = JSON.stringify(this.chartConfig);
|
||||
qs.graph = JSON.stringify(this.chartConfig);
|
||||
my.setHashQueryString(qs);
|
||||
this.redraw();
|
||||
},
|
||||
@@ -147,8 +147,8 @@ my.FlotGraph = Backbone.View.extend({
|
||||
// Uncaught Invalid dimensions for plot, width = 0, height = 0
|
||||
// * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value'
|
||||
var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
|
||||
if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
|
||||
return
|
||||
if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
|
||||
return;
|
||||
}
|
||||
var series = this.createSeries();
|
||||
var options = this.getGraphOptions(this.chartConfig.graphType);
|
||||
@@ -181,7 +181,7 @@ my.FlotGraph = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
};
|
||||
// TODO: we should really use tickFormatter and 1 interval ticks if (and
|
||||
// only if) x-axis values are non-numeric
|
||||
// However, that is non-trivial to work out from a dataset (datasets may
|
||||
@@ -191,21 +191,21 @@ my.FlotGraph = Backbone.View.extend({
|
||||
series: {
|
||||
lines: { show: true }
|
||||
}
|
||||
}
|
||||
, points: {
|
||||
},
|
||||
points: {
|
||||
series: {
|
||||
points: { show: true }
|
||||
},
|
||||
grid: { hoverable: true, clickable: true }
|
||||
}
|
||||
, 'lines-and-points': {
|
||||
},
|
||||
'lines-and-points': {
|
||||
series: {
|
||||
points: { show: true },
|
||||
lines: { show: true }
|
||||
},
|
||||
grid: { hoverable: true, clickable: true }
|
||||
}
|
||||
, bars: {
|
||||
},
|
||||
bars: {
|
||||
series: {
|
||||
lines: {show: false},
|
||||
bars: {
|
||||
@@ -225,7 +225,7 @@ my.FlotGraph = Backbone.View.extend({
|
||||
max: self.model.currentDocuments.length - 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return options[typeId];
|
||||
},
|
||||
|
||||
|
||||
@@ -8,16 +8,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');
|
||||
@@ -26,14 +22,13 @@ my.DataGrid = Backbone.View.extend({
|
||||
this.model.currentDocuments.bind('remove', this.render);
|
||||
this.state = {};
|
||||
this.hiddenFields = [];
|
||||
this.options = options;
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .column-header-menu': 'onColumnHeaderClick'
|
||||
, 'click .row-header-menu': 'onRowHeaderClick'
|
||||
, 'click .root-header-menu': 'onRootHeaderClick'
|
||||
, 'click .data-table-menu li a': 'onMenuClick'
|
||||
'click .column-header-menu': 'onColumnHeaderClick',
|
||||
'click .row-header-menu': 'onRowHeaderClick',
|
||||
'click .root-header-menu': 'onRootHeaderClick',
|
||||
'click .data-table-menu li a': 'onMenuClick'
|
||||
},
|
||||
|
||||
// TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)).
|
||||
@@ -72,33 +67,35 @@ my.DataGrid = Backbone.View.extend({
|
||||
var self = this;
|
||||
e.preventDefault();
|
||||
var actions = {
|
||||
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
|
||||
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); },
|
||||
facet: function() {
|
||||
self.model.queryState.addFacet(self.state.currentColumn);
|
||||
},
|
||||
facet_histogram: function() {
|
||||
self.model.queryState.addHistogramFacet(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') },
|
||||
hideColumn: function() { self.hideColumn() },
|
||||
showColumn: function() { self.showColumn(e) },
|
||||
transform: function() { self.showTransformDialog('transform'); },
|
||||
sortAsc: function() { self.setColumnSort('asc'); },
|
||||
sortDesc: function() { self.setColumnSort('desc'); },
|
||||
hideColumn: function() { self.hideColumn(); },
|
||||
showColumn: function() { self.showColumn(e); },
|
||||
deleteRow: function() {
|
||||
var doc = _.find(self.model.currentDocuments.models, function(doc) {
|
||||
// important this is == as the currentRow will be string (as comes
|
||||
// from DOM) while id may be int
|
||||
return doc.id == self.state.currentRow
|
||||
return doc.id == self.state.currentRow;
|
||||
});
|
||||
doc.destroy().then(function() {
|
||||
self.model.currentDocuments.remove(doc);
|
||||
my.notify("Row deleted successfully");
|
||||
})
|
||||
.fail(function(err) {
|
||||
my.notify("Errorz! " + err)
|
||||
})
|
||||
}).fail(function(err) {
|
||||
my.notify("Errorz! " + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
actions[$(e.target).attr('data-action')]();
|
||||
},
|
||||
|
||||
@@ -114,7 +111,7 @@ my.DataGrid = Backbone.View.extend({
|
||||
$el.append(view.el);
|
||||
util.observeExit($el, function() {
|
||||
util.hide('dialog');
|
||||
})
|
||||
});
|
||||
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
|
||||
},
|
||||
|
||||
@@ -128,7 +125,7 @@ my.DataGrid = Backbone.View.extend({
|
||||
$el.append(view.el);
|
||||
util.observeExit($el, function() {
|
||||
util.hide('dialog');
|
||||
})
|
||||
});
|
||||
$('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
|
||||
},
|
||||
|
||||
@@ -151,7 +148,7 @@ my.DataGrid = Backbone.View.extend({
|
||||
// ======================================================
|
||||
// #### 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}} \
|
||||
@@ -169,7 +166,8 @@ my.DataGrid = Backbone.View.extend({
|
||||
<div class="btn-group column-header-menu"> \
|
||||
<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="facet" href="JavaScript:void(0);">Term Facet</a></li> \
|
||||
<li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</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> \
|
||||
@@ -190,10 +188,10 @@ my.DataGrid = Backbone.View.extend({
|
||||
',
|
||||
|
||||
toTemplateJSON: function() {
|
||||
var modelData = this.model.toJSON()
|
||||
modelData.notEmpty = ( this.fields.length > 0 )
|
||||
var modelData = this.model.toJSON();
|
||||
modelData.notEmpty = ( this.fields.length > 0 );
|
||||
// TODO: move this sort of thing into a toTemplateJSON method on Dataset?
|
||||
modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
|
||||
modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
|
||||
return modelData;
|
||||
},
|
||||
render: function() {
|
||||
@@ -210,12 +208,10 @@ 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));
|
||||
this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
@@ -226,14 +222,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:
|
||||
//
|
||||
// <pre>
|
||||
@@ -241,22 +229,12 @@ my.DataGrid = Backbone.View.extend({
|
||||
// model: dataset-document,
|
||||
// el: dom-element,
|
||||
// fields: mydatasets.fields // a FieldList object
|
||||
// }, {
|
||||
// cellRenderer: my-cell-renderer-function
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// </pre>
|
||||
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);
|
||||
},
|
||||
@@ -291,10 +269,10 @@ 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)
|
||||
}
|
||||
})
|
||||
return { id: this.id, cells: cellData }
|
||||
value: doc.getFieldValue(field)
|
||||
};
|
||||
});
|
||||
return { id: this.id, cells: cellData };
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
@@ -69,7 +69,7 @@ my.Map = Backbone.View.extend({
|
||||
if (!self.mapReady){
|
||||
self._setupMap();
|
||||
}
|
||||
self.redraw()
|
||||
self.redraw();
|
||||
});
|
||||
|
||||
return this;
|
||||
@@ -95,7 +95,6 @@ my.Map = Backbone.View.extend({
|
||||
// Clear and rebuild all features
|
||||
this.features.clearLayers();
|
||||
this._add(this.model.currentDocuments.models);
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -153,9 +152,9 @@ my.Map = Backbone.View.extend({
|
||||
type: 'Point',
|
||||
coordinates: [
|
||||
doc.attributes[this._lonFieldName],
|
||||
doc.attributes[this._latFieldName],
|
||||
doc.attributes[this._latFieldName]
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -132,8 +132,8 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
',
|
||||
|
||||
events: {
|
||||
'click .okButton': 'onSubmit'
|
||||
, 'keydown .expression-preview-code': 'onEditorKeydown'
|
||||
'click .okButton': 'onSubmit',
|
||||
'keydown .expression-preview-code': 'onEditorKeydown'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
@@ -143,7 +143,7 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
render: function() {
|
||||
var htmls = $.mustache(this.template,
|
||||
{name: this.state.currentColumn}
|
||||
)
|
||||
);
|
||||
this.el.html(htmls);
|
||||
// Put in the basic (identity) transform script
|
||||
// TODO: put this into the template?
|
||||
@@ -181,7 +181,7 @@ my.ColumnTransform = Backbone.View.extend({
|
||||
_.each(toUpdate, function(editedDoc) {
|
||||
var realDoc = self.model.currentDocuments.get(editedDoc.id);
|
||||
realDoc.set(editedDoc);
|
||||
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
|
||||
realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
117
src/view.js
117
src/view.js
@@ -54,7 +54,7 @@ this.recline.View = this.recline.View || {};
|
||||
// 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"> \
|
||||
@@ -66,6 +66,10 @@ my.DataExplorer = Backbone.View.extend({
|
||||
<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> \
|
||||
@@ -78,6 +82,9 @@ my.DataExplorer = Backbone.View.extend({
|
||||
</div> \
|
||||
</div> \
|
||||
',
|
||||
events: {
|
||||
'click .menu-right a': 'onMenuClick'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
var self = this;
|
||||
@@ -116,7 +123,7 @@ my.DataExplorer = Backbone.View.extend({
|
||||
my.notify('Data loaded', {category: 'success'});
|
||||
// update navigation
|
||||
var qs = my.parseHashQueryString();
|
||||
qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON());
|
||||
qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
|
||||
var out = my.getNewHashForQueryString(qs);
|
||||
self.router.navigate(out);
|
||||
});
|
||||
@@ -165,7 +172,7 @@ my.DataExplorer = Backbone.View.extend({
|
||||
$(this.el).html(template);
|
||||
var $dataViewContainer = this.el.find('.data-view-container');
|
||||
_.each(this.pageViews, function(view, pageName) {
|
||||
$dataViewContainer.append(view.view.el)
|
||||
$dataViewContainer.append(view.view.el);
|
||||
});
|
||||
var queryEditor = new my.QueryEditor({
|
||||
model: this.model.queryState
|
||||
@@ -174,10 +181,12 @@ my.DataExplorer = Backbone.View.extend({
|
||||
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.$facetViewer = facetViewer.el;
|
||||
this.el.find('.header').append(facetViewer.el);
|
||||
},
|
||||
|
||||
@@ -210,6 +219,16 @@ my.DataExplorer = Backbone.View.extend({
|
||||
view.view.trigger('view: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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -220,28 +239,21 @@ my.QueryEditor = Backbone.View.extend({
|
||||
<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'
|
||||
'submit form': 'onFormSubmit',
|
||||
'click .action-pagination-update': 'onPaginationUpdate'
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
@@ -253,32 +265,21 @@ 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();
|
||||
var $el = $(e.target);
|
||||
var newFrom = 0;
|
||||
if ($el.parent().hasClass('prev')) {
|
||||
var newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
|
||||
newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
|
||||
} else {
|
||||
var newFrom = this.model.get('from') + this.model.get('size');
|
||||
newFrom = this.model.get('from') + this.model.get('size');
|
||||
}
|
||||
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');
|
||||
@@ -311,7 +312,8 @@ my.FilterEditor = Backbone.View.extend({
|
||||
</div> \
|
||||
{{/termFilters}} \
|
||||
</div> \
|
||||
<div class="span2"> \
|
||||
<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> \
|
||||
@@ -347,7 +349,7 @@ my.FilterEditor = Backbone.View.extend({
|
||||
fieldId: fieldId,
|
||||
label: fieldId,
|
||||
value: filter.term[fieldId]
|
||||
}
|
||||
};
|
||||
});
|
||||
var out = $.mustache(this.template, tmplData);
|
||||
this.el.html(out);
|
||||
@@ -398,8 +400,11 @@ my.FacetViewer = Backbone.View.extend({
|
||||
<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}} \
|
||||
{{#entries}} \
|
||||
<li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
|
||||
{{/entries}} \
|
||||
</ul> \
|
||||
</div> \
|
||||
{{/facets}} \
|
||||
@@ -408,7 +413,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');
|
||||
@@ -422,6 +427,15 @@ my.FacetViewer = Backbone.View.extend({
|
||||
facets: this.model.facets.toJSON(),
|
||||
fields: this.model.fields.toJSON()
|
||||
};
|
||||
tmplData.facets = _.map(tmplData.facets, function(facet) {
|
||||
if (facet._type === 'date_histogram') {
|
||||
facet.entries = _.map(facet.entries, function(entry) {
|
||||
entry.term = new Date(entry.time).toDateString();
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
return facet;
|
||||
});
|
||||
var templated = $.mustache(this.template, tmplData);
|
||||
this.el.html(templated);
|
||||
// are there actually any facets to show?
|
||||
@@ -436,10 +450,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);
|
||||
}
|
||||
});
|
||||
@@ -452,15 +465,15 @@ 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) {
|
||||
if (parsed === null) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
path: parsed[1],
|
||||
query: parsed[2] || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Parse a URL query string (?xyz=abc...) into a dictionary.
|
||||
my.parseQueryString = function(q) {
|
||||
@@ -481,13 +494,13 @@ my.parseQueryString = function(q) {
|
||||
urlParams[d(e[1])] = d(e[2]);
|
||||
}
|
||||
return urlParams;
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
@@ -498,7 +511,7 @@ my.composeQueryString = function(queryParams) {
|
||||
});
|
||||
queryString += items.join('&');
|
||||
return queryString;
|
||||
}
|
||||
};
|
||||
|
||||
my.getNewHashForQueryString = function(queryParams) {
|
||||
var queryPart = my.composeQueryString(queryParams);
|
||||
@@ -508,11 +521,11 @@ my.getNewHashForQueryString = function(queryParams) {
|
||||
} else {
|
||||
return queryPart;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
my.setHashQueryString = function(queryParams) {
|
||||
window.location.hash = my.getNewHashForQueryString(queryParams);
|
||||
}
|
||||
};
|
||||
|
||||
// ## notify
|
||||
//
|
||||
@@ -522,7 +535,7 @@ my.setHashQueryString = function(queryParams) {
|
||||
// * persist: if true alert is persistent, o/w hidden after 3s (default = false)
|
||||
// * loader: if true show loading spinner
|
||||
my.notify = function(message, options) {
|
||||
if (!options) var options = {};
|
||||
if (!options) options = {};
|
||||
var tmplData = _.extend({
|
||||
msg: message,
|
||||
category: 'warning'
|
||||
@@ -536,7 +549,7 @@ my.notify = function(message, options) {
|
||||
{{/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() {
|
||||
@@ -544,15 +557,15 @@ my.notify = function(message, options) {
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ## 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();
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, recline.View);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user