Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -0,0 +1,62 @@
const _ = require('underscore');
module.exports = {
sortSchema: function (schema) {
var priorities = {
'cartodb_id': 1,
'the_geom': 2,
'__default__': 3,
'created_at': 4,
'updated_at': 5
};
function priority (v) {
return priorities[v] || priorities['__default__'];
}
return _.chain(schema)
.clone()
.sort(function (a, b) { // ..and then re-sort by priorities defined above
var prioA = priority(a[0]);
var prioB = priority(b[0]);
if (prioA < prioB) {
return -1;
} else if (prioA > prioB) {
return 1;
} else { // priority is the same (i.e. __default__), so compare alphabetically
return a[0] < b[0] ? -1 : 1;
}
})
.value();
},
/**
* return true if the sql query alters table schema in some way
*/
alterTable: function (sql) {
sql = sql.trim();
return sql.search(/alter\s+[\w\."]+\s+/i) !== -1 ||
sql.search(/drop\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^vacuum\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^create\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^reindex\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^grant\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^revoke\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^cluster\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^comment\s+on\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^explain\s+[\w\.\"]+/i) !== -1;
},
/**
* return true if the sql query alters table data
*/
alterTableData: function (sql) {
return this.alterTable(sql) ||
sql.search(/^refresh\s+materialized\s+view\s+[\w\.\"]+/i) !== -1 ||
sql.search(/^truncate\s+[\w\.\"]+/i) !== -1 ||
sql.search(/insert\s+into/i) !== -1 ||
sql.search(/update\s+[\w\.\-"]+\s+.*set/i) !== -1 ||
sql.search(/delete\s+from/i) !== -1;
}
};
@@ -0,0 +1,835 @@
const _ = require('underscore');
const $ = require('jquery');
const Backbone = require('backbone');
const SQL = require('internal-carto.js').SQL;
const safeTableNameQuoting = require('dashboard/helpers/safe-table-name-quoting');
const cartoMetadataStatic = require('./carto-table-metadata-static');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const CartoTableData = require('dashboard/data/table/carto-table-data');
const ColumnModel = require('dashboard/data/table/column-model');
const RowModel = require('dashboard/data/table/row-model');
const PermissionModel = require('dashboard/data/permission-model');
const ImportModel = require('dashboard/data/import-model');
const TableSynchronizationModel = require('dashboard/data/table-synchronization-model');
const getSimpleGeometryType = require('builder/data/get-simple-geometry-type');
const retrigger = require('dashboard/helpers/retrigger');
const REQUIRED_OPTS = [
'configModel'
];
const Base = Backbone.Model.extend({
columnNames: function () {
return _.map(this.get('schema'), function (c) {
return c[0];
});
},
columnName: function (idx) {
return this.columnNames()[idx];
}
});
const CartoTableMetadata = Base.extend({
currentLoading: 0, // class variable (shared). I'm still not sure if this is messy as hell or powerfull as a transformer
_TEXTS: {
columnDeleted: 'Your column has been deleted',
columnDeleting: 'Deleting your column',
columnAdded: 'Your column has been added',
columnAdding: 'Adding new column'
},
hiddenColumns: [
'the_geom',
'the_geom_webmercator',
'cartodb_georef_status',
'created_at',
'updated_at',
'cartodb_id'
],
initialize: function (attrs, opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
_.bindAll(this, 'notice');
this.readOnly = false;
this.bind('change:schema', this._prepareSchema, this);
this._prepareSchema();
this.sqlView = null;
this.synchronization = new TableSynchronizationModel({ configModel: opts.configModel });
this.synchronization.linkToTable(this);
this.synchronization.bind('change:id', function isSyncChanged () {
this.trigger('change:isSync', this, this.synchronization.isSync());
}, this);
if (this.get('no_data_fetch')) {
this.no_data_fetch = true;
delete this.attributes.no_data_fetch;
}
this.data();
this.bind('error', function (e, resp) {
this.error('', resp);
}, this);
this._data.bind('error', function (e, resp) {
this.notice('error loading rows, check your SQL query', 'error', 5000);
}, this);
this._data.bind('reset', function () {
var view = this._data;
this.set({
schema: view.schemaFromData(this.get('schema')),
geometry_types: view.getGeometryTypes()
});
}, this);
retrigger.call(this, 'change', this._data, 'data:changed');
retrigger.call(this, 'saved', this._data, 'data:saved');
this.bind('change:table_visualization', function () {
this.permission = new PermissionModel(this.get('table_visualization').permission, { configModel: this._configModel });
this.trigger('change:permission', this, this.permission);
}, this);
// create permission if permission is set
this.permission = new PermissionModel(this.get('permission'), { configModel: this._configModel });
},
url: function (method) {
var version = this._configModel.urlVersion('table', method);
var base = '/api/' + version + '/tables';
if (this.isNew()) {
return base;
}
return base + '/' + this.id;
},
// use the name as the id since the api works
// in the same way to table name and id
parse: function (resp, xhr) {
if (resp.name) {
resp.id = resp.name;
}
// move geometry_types to stats one
// geometry_types from backend are not reliable anymore and it can only be used
// for non editing stuff (showing icons, general checks on table list)
resp.stats_geometry_types = resp.geometry_types;
delete resp.geometry_types;
delete resp.schema;
return resp;
},
notice: function (msg, type, timeout) {
this.trigger('notice', msg, type, timeout);
},
setReadOnly: function (_) {
var trigger = false;
if (this.readOnly !== _) {
trigger = true;
}
this.readOnly = _;
if (trigger) {
this.trigger('change:readOnly', this, _);
}
},
isReadOnly: function () {
return this.readOnly || this.data().isReadOnly() || this.synchronization.isSync();
},
isSync: function () {
return this.synchronization.isSync();
},
getUnqualifiedName: function () {
var name = this.get('name');
if (!name) return null;
var tk = name.split('.');
if (tk.length == 2) { // eslint-disable-line eqeqeq
return tk[1];
}
return name;
},
// "user".table -> user.table
getUnquotedName: function () {
var name = this.get('name');
return name && name.replace(/"/g, '');
},
sortSchema: function () {
this.set('schema', CartoTableMetadata.sortSchema(this.get('schema')));
},
error: function (msg, resp) {
let err = '';
try {
err = resp && resp.responseText && JSON.parse(resp.responseText).errors[0];
} catch (e) {
} finally {
this.trigger('notice', msg + ': ' + err, 'error');
}
},
_prepareSchema: function () {
this._columnType = {};
_(this.get('schema')).each((schema) => {
this._columnType[schema[0]] = schema[1];
});
if (!this.isInSQLView()) {
this.set('original_schema', this.get('schema'));
}
},
columnNames: function (sc) {
sc = sc || 'schema';
return _(this.get(sc)).pluck(0);
},
containsColumn: function (name) {
return _.contains(this.columnNames(), name);
},
columnNamesByType: function (type, sc) {
sc = sc || 'schema';
var t = _(this.get(sc)).filter(function (c) {
return c[1] == type; // eslint-disable-line eqeqeq
});
return _(t).pluck(0);
},
// return geometry columns calculated backend stats
// use geomColumnTypes if you need something reliable (but slower and async)
statsGeomColumnTypes: function (geometryTypes) {
return this.geomColumnTypes(this.get('stats_geometry_types'));
},
// return the current column types in an array
// the values inside the array can be:
// 'point', 'line', 'polygon'
geomColumnTypes: function (geometryTypes) {
var types = geometryTypes || this.get('geometry_types');
var geomTypes = [];
if (!_.isArray(types)) {
return [];
}
var _map = {
'st_multipolygon': 'polygon',
'st_polygon': 'polygon',
'st_multilinestring': 'line',
'st_linestring': 'line',
'st_multipoint': 'point',
'st_point': 'point'
};
for (var t in types) {
var type = types[t];
// when there are rows with no geo type null is returned as geotype
if (type) {
var a = _map[type.toLowerCase()];
if (a) {
geomTypes.push(a);
}
}
}
return _.uniq(geomTypes);
},
/**
* Adding a new geometry type to the table
* @param geom type {st_polygon, st_point,...}
* @param set options
*/
addGeomColumnType: function (t, opts) {
if (!t) return;
var types = _.clone(this.get('geometry_types')) || [];
if (!_.contains(types, t)) {
types.push(t);
this.set({
'geometry_types': types
}, opts);
}
},
nonReservedColumnNames: function () {
return _.filter(this.columnNames(), (columnName) => {
return !this.isReservedColumn(columnName);
});
},
columnTypes: function () {
return _.clone(this._columnType);
},
_getColumn: function (columnName) {
if (this._columnType[columnName] === undefined) {
return;
// throw "the column does not exists";
}
var c = new ColumnModel({
table: this,
name: columnName,
type: this._columnType[columnName],
configModel: this._configModel
}, { configModel: this._configModel });
return c;
},
getColumnType: function (columnName, sc) {
sc = sc || 'schema';
var t = _(this.get(sc)).filter(function (c) {
return c[0] == columnName;// eslint-disable-line eqeqeq
});
if (t.length > 0) {
return t[0][1];
}
},
addColumn: function (columnName, columnType, opts) {
var c = new ColumnModel({
table: this,
_name: columnName,
type: columnType || 'string',
configModel: this._configModel
}, { configModel: this._configModel });
this.notice(this._TEXTS.columnAdding, 'load', 0);
c.save(null, {
success: (model, obj) => {
this.notice(this._TEXTS.columnAdded, 'info');
this.trigger('columnAdd', columnName);
this.data().fetch();
opts && opts.success && opts.success(model, obj);
},
error: (error, resp) => {
this.error('error adding column', resp);
opts && opts.error && opts.error(error);
},
wait: true
});
},
deleteColumn: function (columnName, opts) {
var c = this._getColumn(columnName);
if (c !== undefined) {
this.notice(this._TEXTS.columnDeleting, 'load', 0);
c.destroy({
success: () => {
this.trigger('columnDelete', columnName);
this.notice(this._TEXTS.columnDeleted, 'info');
this.data().fetch();
opts && opts.success && opts.success();
},
error: (e, resp) => {
this.error('error deleting column', resp);
opts && opts.error && opts.error();
},
wait: true
});
}
},
renameColumn: function (columnName, newName, opts) {
if (columnName == newName) return; // eslint-disable-line eqeqeq
var c = this._getColumn(columnName);
var oldName = c.get('name');
c.set({
new_name: newName,
old_name: c.get('name')
});
this.notice('renaming column', 'load', 0);
c.save(null, {
success: (mdl, data) => {
this.notice('Column has been renamed', 'info');
this.trigger('columnRename', newName, oldName);
this.data().fetch();
opts && opts.success && opts.success(mdl, data);
},
error: (e, resp) => {
console.error("can't rename column");
this.error('error renaming column', resp);
opts && opts.error && opts.error(e, resp);
},
wait: true
});
},
isTypeChangeAllowed: function (columnName, newType) {
var deactivateMatrix = {
'number': ['date'],
'boolean': ['date'],
'date': ['boolean']
};
var c = this._getColumn(columnName);
if (!c) {
return true;
}
var type = c.get('type');
var deactivated = deactivateMatrix[type] || [];
deactivated = deactivated.concat([type]);
return !_.contains(deactivated, newType);
},
isTypeChangeDestructive: function (columnName, newType) {
var columnType = this.getColumnType(columnName);
var destructiveMatrix = {
'string': {
'string': false,
'number': true,
'date': true,
'boolean': true
},
'number': {
'string': false,
'number': false,
'date': true,
'boolean': true
},
'date': {
'string': false,
'number': true,
'date': false,
'boolean': true
},
'boolean': {
'string': false,
'number': false,
'date': true,
'boolean': false
}
};
return destructiveMatrix[columnType][newType];
},
changeColumnType: function (columnName, newType, opts) {
var c = this._getColumn(columnName);
if (this.getColumnType(columnName) == newType) { // eslint-disable-line eqeqeq
opts && opts.success && opts.success();
return;
}
this.saveNewColumnType(c, newType, opts);
},
saveNewColumnType: function (column, newType, opts) {
column.set({ type: newType });
this.notice('Changing column type', 'load', 0);
column.save(null, {
success: () => {
this.notice('Column type has been changed', 'info');
this.trigger('typeChanged', newType); // to make it testable
this.data().fetch();
opts && opts.success && opts.success();
},
error: (e, resp) => {
this.trigger('typeChangeFailed', newType, e); // to make it testable
this.error('error changing column type', resp);
opts && opts.error && opts.error(e, resp);
},
wait: true
});
},
/**
* returns the original data for the table not the current applied view
*/
originalData: function () {
return this._data;
},
data: function () {
if (this._data === undefined) {
this._data = new CartoTableData(null, {
table: this,
configModel: this._configModel
});
this.bindData();
}
if (this.sqlView) {
return this.sqlView;
}
return this._data;
},
bindData: function (data) {
if (this._data && !this._data.bindedReset) {
retrigger.call(this, 'sync', this._data, 'dataLoaded');
retrigger.call(this, 'add', this._data, 'dataAdded');
this._data.bindedReset = true;
}
if (this.sqlView && !this.sqlView.bindedReset) {
retrigger.call(this, 'sync', this.sqlView, 'dataLoaded');
retrigger.call(this, 'add', this.sqlView, 'dataAdded');
this.sqlView.bindedReset = true;
}
},
useSQLView: function (view, options) {
if (!view && !this.sqlView) return;
options = options || {};
if (this.sqlView) {
this.sqlView.unbind(null, null, this);
this.sqlView.unbind(null, null, this._data);
}
// reset previous
if (!view && this.sqlView) {
this.sqlView.table = null;
}
this.sqlView = view;
this.bindData();
if (view) {
view.bind('sync reset', function () {
if (!view.modify_rows) {
this.set({
schema: view.schemaFromData(this.get('schema')),
geometry_types: view.getGeometryTypes()
});
}
}, this);
// listen for errors
view.bind('error', function (e, resp) {
this.notice('error loading rows, check your SQL query', 'error', 5000);
}, this);
view.bind('loading', function () {
// this.notice(_t('loading query'), 'load', 0);
}, this);
view.bind('reset loaded', function () {
if (view.modify_rows) {
this.notice(view.affected_rows + ' rows affected');
this.useSQLView(null);
} else {
this.notice(_t('loaded'));
}
}, this);
// swicth source data
this.dataModel = this.sqlView;
view.table = this;
} else {
this.dataModel = this._data;
// get the original schema
this.set({
'schema': this.get('original_schema')
});/// *, { silent: true });
this.data().fetch();
}
this.trigger('change:dataSource', this.dataModel, this);
},
isInSQLView: function () {
return !!this.sqlView;
},
/**
* replace fetch functionally to add some extra call for logging
* it can be used in the same way fetch is
*/
fetch: function (opts) {
const silent = opts ? opts.silent : false;
if (!silent) {
this.notice('loading table', 'load', this, 0, 0);
}
var xhr = Base.prototype.fetch.call(this, opts);
$.when(xhr).done(() => {
opts && opts.success && opts.success.old_success && opts.success.old_success();
if (!silent) {
this.notice('loaded');
}
}).fail(() => {
if (!silent) {
this.notice('error loading the table');
}
});
return xhr;
},
isReservedColumn: function (c) {
return RowModel.isReservedColumn(c);
},
/**
* when a table is linked to a infowindow each time a column
* is renamed or removed the table pings to infowindow to remove
* or rename the fields
*/
linkToInfowindow: function (infowindow) {
this.bind('columnRename', function (newName, oldName) {
if (infowindow.containsField(oldName)) {
infowindow.removeField(oldName);
infowindow.addField(newName);
}
}, infowindow);
this.bind('columnDelete', function (oldName, newName) {
infowindow.removeField(oldName);
}, infowindow);
this.bind('change:schema', function () {
var columns = _(this.columnNames()).filter(function (c) {
return !_.contains(infowindow.SYSTEM_COLUMNS, c);
});
function _hash (str) {
var hash = 0;
var c;
var i;
for (i = 0; i < str.length; i++) {
c = str.charCodeAt(i);
hash = c + (hash << 6) + (hash << 16) - hash;
}
return hash;
}
if (this.isInSQLView()) {
if (!infowindow.has('defaul_schema_fields')) {
infowindow.saveFields('defaul_schema_fields');
}
var current_schema_key = 'schema_' + _hash(this.columnNames().sort().join(''));
var previous_schema_key = 'schema_' + _hash(
_(this.previous('schema')).pluck(0).sort().join('')
);
if (!infowindow.has(previous_schema_key)) {
infowindow.saveFields(previous_schema_key);
}
if (infowindow.has(current_schema_key)) {
infowindow.restoreFields(null, current_schema_key);
}
} else {
infowindow.restoreFields(null, 'defaul_schema_fields');
}
if (infowindow.get('template')) {
// merge fields checking actual schema
infowindow.mergeFields(columns);
} else {
// remove fields that no longer exist
infowindow.removeMissingFields(columns);
}
}, this);
},
embedURL: function () {
return '/tables/' + this.get('name') + '/embed_map';
},
/**
* @deprecated use vis.viewUrl() or vis.viewUrl(currentUser) instead.
*/
viewUrl: function () {
return this._configModel.prefixUrl() + '/tables/' + this.getUnqualifiedName();
},
hasTheGeom: function () {
var currentSchema = this.get('schema');
// if we have "the_geom" in our current schema, returnstrue
for (var n in currentSchema) {
if (currentSchema[n][0] === 'the_geom') {
return true;
}
}
return false;
},
/**
* Checks the server to see if the table has any georeferenced row, independently of the applyed query
* @return {promise}
*/
fetchGeoreferenceStatus: function () {
var dfd = $.Deferred();
var username = (this.options && this.options.user_data) ? this.options.user_data.username
: (window.user_data ? window.user_data.username : window.user_name);
var api_key = (this.options && this.options.user_data) ? this.options.user_data.api_key
: (window.user_data ? window.user_data.api_key : window.api_key);
this.sqlApi = new SQL({
user: username,
version: 'v1',
api_key: api_key,
sql_api_template: this._configModel.getSqlApiBaseUrl()
});
var sql = 'SELECT the_geom FROM ' + this.get('name') + ' WHERE the_geom is not null';
this.sqlApi.execute(sql).done(function (data) {
if (data.rows.length > 0) {
dfd.resolve(true);
} else {
dfd.resolve(false);
}
});
return dfd.promise();
},
/**
* Checks the current loaded records to see if they are georeferenced
* @return {boolean}
*/
isGeoreferenced: function () {
var geoColumns = this.geomColumnTypes();
if (geoColumns && geoColumns.length > 0) {
return true;
} else {
if (!this.isInSQLView()) {
// sometimes the columns are changed in the frontend site
// and the geocolumns are not updated.
// check the columns in local
return this._data.any(function (row) {
return row.hasGeometry();
});
}
}
return false;
},
/**
* this function can only be called during change event
* returns true if the geometry type has changed
* for this method multipolygon and polygon are the same geometry type
*/
geometryTypeChanged: function () {
if (!('geometry_types' in this.changed)) return false;
var geoTypes = this.get('geometry_types');
var prevGeoTypes = this.previousAttributes().geometry_types;
function normalize (e) {
e = e.toLowerCase();
if (e === 'st_multipolygon') {
return 'st_polygon';
}
if (e === 'st_multilinestring') {
return 'st_linestring';
}
if (e === 'st_multipoint') {
return 'st_point';
}
return e;
}
if (!geoTypes ||
geoTypes.length === 0 ||
!prevGeoTypes ||
prevGeoTypes.length === 0) {
return true;
}
var n = normalize(geoTypes[0]);
var o = normalize(prevGeoTypes[0]);
return n !== o;
},
/**
* Get necessary data create a duplicated dataset from this table.
*
* @param {Object} newName name of new dataset.
* @param {Object} callbacks
* @returns {Object}
*/
duplicate: function (newName, callbacks) {
callbacks = callbacks || {};
// Extracted from duplicate_table_dialog
var data = {
table_name: newName
};
// Set correct data object, depending on if the app has a query applied or not
if (this.isInSQLView()) {
var query = this.data().getSQL();
data.sql = (!query || query == '') ? 'SELECT * FROM ' + safeTableNameQuoting(this.get('name')) : query; // eslint-disable-line eqeqeq
} else {
data.table_copy = this.get('name');
}
var importModel = new ImportModel();
importModel.save(data, {
error: callbacks.error,
success: (model, changes) => {
var checkImportModel = new ImportModel({
item_queue_id: changes.item_queue_id
});
checkImportModel.bind('importComplete', () => {
checkImportModel.unbind();
// So import is done, create new table object from the new table and fetch, callback once finished.
var newTable = new CartoTableMetadata({
id: checkImportModel.get('table_id')
}, { configModel: this._configModel });
newTable.fetch({
success: function () {
callbacks.success(newTable);
},
error: callbacks.error
});
});
checkImportModel.bind('importError', function () {
checkImportModel.unbind();
callbacks.error.apply(this, arguments);
});
checkImportModel.pollCheck();
}
});
},
/**
* Get the visualizations that are using this table dataset.
* Note! a .fetch() is required to be sure the data to be available.
* @return {Array}
*/
dependentVisualizations: function () {
// dependent = visualizations with a single layer
// non-dependant = have more than this dataset as a layer
return _.chain(this.get('dependent_visualizations'))
.union(this.get('non_dependent_visualizations'))
.compact()
.value() || [];
},
getGeometryType: function () {
var types = this.get('geometry_types');
var geomTypes = [];
if (!_.isArray(types)) {
return [];
}
for (var t in types) {
var type = types[t];
// when there are rows with no geo type null is returned as geotype
if (type) {
var a = getSimpleGeometryType(type.toLowerCase());
if (a) {
geomTypes.push(a);
}
}
}
return _.uniq(geomTypes);
}
}, {
/**
* creates a new table from query
* the called is responsable of calling save to create
* the table in the server
*/
createFromQuery: function (name, query, configModel) {
return new CartoTableMetadata({
sql: query,
name: name
}, { configModel });
},
...cartoMetadataStatic
});
module.exports = CartoTableMetadata;
@@ -0,0 +1,34 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral Dialog-headerIcon--small">
<i class="CDB-IconFont CDB-IconFont-markup"></i>
</div>
<p class="Dialog-headerTitle">Query this table</p>
<p class="Dialog-headerText">Using the <a href="https://docs.carto.com/cartodb-platform/sql-api.html" target="_blank">CARTO SQL API</a> you can perform any SQL query to either yours or other users tables.</p>
</div>
<div class="Dialog-body Dialog-body--tall u-inner OptionCards">
<code class="OptionCard OptionCard--static OptionCard--code OptionCard--codeRequest">
<pre><%- url %>?q=<%- sql %></pre>
</code>
<code class="OptionCard OptionCard--static OptionCard--code OptionCard--codeResult">
<pre>{
rows: [<% _.each(rows, function(row, i) { %>
{<% var j = 0 %><% _.each(row.attributes, function(val, key) { %>
"<%- key %>": "<%- val %>"<% if (j !== _.size(row.attributes)-1) { %>,<% } %><% j++ %><% }) %>
}<% if (i !== rows.length-1) { %>,<% } %><% }) %>
}],
time: 0.013,
fields: {<% _.each(schema, function(field, i) { %>
"<%- field[0] %>": { "type": "<%- field[1] %>" }<% if (i !== schema.length-1) { %>,<% } %><% }) %>
},
total_rows: 20
}</pre>
<div class="OptionCard-shadow"></div>
</code>
</div>
<div class="Dialog-footer Dialog-footer--simple u-inner">
<button class="CDB-Text CDB-Button CDB-Button--secondary u-upperCase CDB-Size-medium cancel js-close">
<span>Close</span>
</button>
</div>
@@ -0,0 +1,272 @@
const _ = require('underscore');
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const failTemplate = require('dashboard/components/fail.tpl');
const REQUIRED_OPTS = [
'configModel',
'modalModel'
];
/**
* shows a dialog to get the table exported
* new ExportDialog({
* table: table_model
* })
*
* (Migrated almost-as-is from old editor to not break functionality)
*/
module.exports = CoreView.extend({
_CSV_FILTER: 'SELECT * FROM (%%sql%%) as subq ',
_MAX_SQL_GET_LENGTH: 1000,
events: {
'click .js-option:not(.is-disabled)': '_export',
'click .js-cancel': '_closeDialog'
},
/**
* Allowed formats on the exporter
* @type {Array}
*/
formats: [
{ format: 'csv', fetcher: 'fetchCSV', geomRequired: false, illustrationIconModifier: 'IllustrationIcon--neutral' },
{ format: 'shp', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--magenta' },
{ format: 'kml', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--sunrise' },
{ format: 'geojson', label: 'geo json', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--cyan' },
{ format: 'svg', fetcher: 'fetchSVG', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--royalDark' },
{ format: 'gpkg', fetcher: 'fetch', geomRequired: true, illustrationIconModifier: 'IllustrationIcon--smaltBlue' }
],
initialize: function () {
checkAndBuildOpts(this.options, REQUIRED_OPTS, this);
_.extend(this.options, {
clean_on_hide: true,
table_id: this.model.id
});
_.bindAll(this, '_export');
this.baseUrl = this._configModel.getSqlApiUrl();
this.model.bind('change:geometry_types', this.refresh, this);
},
/**
* search a format based on its name in the format array
* @param {string} format Format name
* @return {Object}
*/
getFormat: function (format) {
for (var n in this.formats) {
if (this.formats[n].format === format) {
return this.formats[n];
}
}
},
/**
* Answer to button event and lauchn the export method associated to that format
* @param {Event} ev
*/
_export: function (ev) {
this.killEvent(ev);
var $button = $(ev.currentTarget);
var formatName = $button.data('format');
var format = this.getFormat(formatName);
this[format.fetcher](formatName);
},
/**
* Create a dictionary with the options shared between all the methods
* @return {Object}
*/
getBaseOptions: function () {
var options = {};
options.filename = this.model.get('name');
if (this.options.user_data) {
options.api_key = this.options.user_data.api_key;
}
return options;
},
/**
* Returns the base sql to retrieve the data
* @return {string}
*/
getPlainSql: function () {
throw new Error('Method not migrated, only used in subclass');
},
/**
* Returns a specific sql filtered by the_geom, used on CSV exports
* @return {string}
*/
getGeomFilteredSql: function () {
var sql = this.getPlainSql();
// if we have "the_geom" in our current schema, we apply a custom sql
if (this.model.isGeoreferenced()) {
return this._CSV_FILTER.replace(/%%sql%%/g, sql);
}
// Otherwise, we apply regular sql
return sql;
},
/**
* Populates the hidden form with the format related values and submits them to get the file
* @param {Object} options Base options
* @param {String} sql Sql of the document to be retrieved
*/
_fetch: function (options, sql) {
this._showElAndHideRest('.js-preparing-download');
this.$('.format').val(options.format);
this.$('.q').val(sql);
this.$('.filename').val(options.filename);
this.$('.api_key').val(options.api_key);
if (options.format === 'csv') {
this.$('.skipfields').val('the_geom_webmercator');
} else {
this.$('.skipfields').val('the_geom,the_geom_webmercator');
}
// check if the sql is big or not, and send the request as a verb or other. This is a HACK.
if (sql.length < this._MAX_SQL_GET_LENGTH) {
var location = this.$('form').attr('action') + '?' + this.$('form').serialize();
this._fetchGET(location);
} else {
// I can't find a way of making the iframe trigger load event when its get a form posted,
// so we need to leave like it was until
this.submit();
}
this.$('.db').attr('disabled', 'disabled');
this.$('.skipfields').attr('disabled', 'disabled');
if (this.options.autoClose) {
this.close();
this.trigger('generating', this.$('.js-preparing-download').html());
}
},
showError: function (error) {
this.$('.js-error').html(
failTemplate({
msg: error
})
);
this._showElAndHideRest('.js-error');
},
_fetchGET: function (url) {
function getError (content) {
// sql api returns a json when it fails
// but if the browser is running some plugin that
// formats it, the window content is the html
// so search for the word "error"
var error = null;
try {
var json = JSON.parse(content);
error = json.error[0];
} catch (e) {
if (content && content.indexOf('error') !== -1) {
error = 'an error occurred';
}
}
return error;
}
var checkInterval;
var w = window.open(url);
w.onload = () => {
clearInterval(checkInterval);
var error = getError(w.document.body.textContent);
if (error) {
this.showError(error);
} else {
this._modalModel.destroy();
}
w.close();
};
window.focus();
checkInterval = setInterval(() => {
// safari needs to check the body because it never
// calls onload
if (w.closed || (w.document && w.document.body.textContent.length === 0)) {
this._modalModel.destroy();
clearInterval(checkInterval);
}
}, 100);
},
/**
* Submits the form. This method is separated to ease the testing
*/
submit: function () {
this.$('form').submit();
},
/**
* Base fetch, for the formats that don't require special threatment
* @param {String} formatName
*/
fetch: function (formatName) {
var options = this.getBaseOptions();
options.format = formatName;
var sql = this.getPlainSql();
this._fetch(options, sql);
},
/**
* Gets the options needed for csv format and fetch the document
* @param {String} formatName
*/
fetchCSV: function () {
var options = this.getBaseOptions();
options.format = 'csv';
var sql = this.getGeomFilteredSql();
this.$('.skipfields').removeAttr('disabled');
this._fetch(options, sql);
},
/**
* Gets the options needed for svg format and fetch the document
* @param {String} formatName
*/
fetchSVG: function () {
this.$('.db').removeAttr('disabled');
this.fetch('svg');
},
/**
* Formerly render_content
* Returns the html populated with current data
* @return {String}
*/
render: function () {
throw new Error('Method not migrated, only used in subclass');
},
refresh: function () {
this.$('.content').html(this.render_content());
},
_renderLoadingContent: function (title) {
return loadingTemplate({
title: title,
descHTML: randomQuote()
});
},
_showElAndHideRest: function (classNameToShow) {
[
'.js-start',
'.js-preparing-download',
'.js-error'
].forEach(function (className) {
this.$(className)[ className === classNameToShow ? 'show' : 'hide' ]();
}, this);
}
});
@@ -0,0 +1,58 @@
<div class="js-start">
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-cloudDownArrow"></i>
</div>
<p class="Dialog-headerTitle">Export dataset</p>
<p class="Dialog-headerText">Select the preferred file format.</p>
<% if (!isGeoreferenced) { %>
<p class="Dialog-headerText">To download any geospatial format like SHP, KML or GeoJSON don't forget to select the_geom on your query.</p>
<% } %>
</div>
<div class="CDB-Text Dialog-body">
<% if (hasBounds) { %>
<div class="OptionCheck">
<div class="Checkbox js-bounds">
<button class="Checkbox-input is-checked"></button>
<label class="Checkbox-label"><strong class="Checkbox-strong">Match rows with the map view.</strong> This option reduces file size.</label>
</div>
</div>
<% } %>
<div class="OptionCards">
<% _.each( formats, function( format ){ %>
<div data-format="<%- format.format %>"
class="js-option OptionCard OptionCard--onlyIcons <%
if (isGeoreferenced === false && format.geomRequired === true) { %> is-disabled <% }
%>">
<div class="IllustrationIcon <%- format.illustrationIconModifier %>">
<div class="IllustrationIcon-text"><%- format.label || format.format %></div>
</div>
</div>
<% }); %>
</div>
</div>
<div class="Dialog-footer Dialog-footer--simple u-inner">
<button class="CDB-Text CDB-Button CDB-Button--secondary CDB-Size-medium u-upperCase cancel js-close">
<span>Cancel</span>
</button>
</div>
</div>
<div class="CDB-Text js-preparing-download" style="display: none;">
<%- preparingDownloadContent %>
</div>
<div class="CDB-Text js-error" style="display: none;"></div>
<form class="hack" method="POST" action="<%- url %>">
<input type="hidden" class="filename" name="filename" />
<input type="hidden" class="q" name="q" />
<input type="hidden" class="format" name="format" />
<input type="hidden" class="bounds" name="bounds" />
<input type="hidden" class="api api_key" name="api_key" />
<input type="hidden" class="skipfields" name="skipfields" disabled="disabled" value="" />
<input type="hidden" class="dp" name="dp" value="4" disabled="disabled" />
</form>
@@ -0,0 +1,111 @@
const _ = require('underscore');
const $ = require('jquery');
const ExportView = require('./export-view');
const templatePublicExport = require('./public-export-template.tpl');
/**
* Shows a dialog to get the public table exported
*
* new PublicExportView({
* table: table_model
* })
*
*/
module.exports = ExportView.extend({
// Events have to be extended from export view parent
events: {
'click .js-option:not(.is-disabled)': '_export',
'click .js-bounds': '_changeBounds',
'click .cancel': '_cancel',
'click .close': '_cancel'
},
initialize: function () {
ExportView.prototype.initialize.call(this);
this.model.set('bounds', this.options.bounds);
this.model.bind('change:bounds', this._setBoundsCheckbox, this);
},
_changeBounds: function () {
this.model.set('bounds', !this.model.get('bounds'));
},
_setBoundsCheckbox: function () {
this.$('.js-bounds .Checkbox-input').toggleClass('is-checked', !!this.model.get('bounds'));
},
/**
* Toggle the bounds option to download the intersect or all the geometries
* @param {Event} ev
*/
_toggleBounds: function (ev) {
this.killEvent(ev);
var $button = $(ev.currentTarget);
var formatName = $button.data('format');
var format = this.getFormat(formatName);
this[format.fetcher](formatName);
},
/**
* Create a dictionary with the options shared between all the methods
* @return {Object}
*/
getBaseOptions: function () {
var options = {};
options.filename = this.model.get('name');
// Keep dataset part in user.dataset names
if (options.filename.indexOf('.') !== -1) {
options.filename = options.filename.split('.')[1];
}
if (this.options.user_data) {
options.api_key = this.options.user_data.api_key;
}
return options;
},
/**
* Returns the base sql to retrieve the data
* @return {string}
*/
getPlainSql: function () {
let sql;
if (this.options.sql) {
sql = this.options.sql;
} else {
if (this.model.sqlView && this.model.get('bounds')) {
sql = this.model.sqlView.getSQL();
} else {
sql = 'select * from ' + this.model.get('name');
}
}
return sql;
},
/**
* Returns the html populated with current data
* @return {String}
*/
render: function () {
var isGeoreferenced = this.model.isGeoreferenced();
var hasBounds = this.model.get('bounds');
if (_.isBoolean(isGeoreferenced)) {
this.$el.html(templatePublicExport({
preparingDownloadContent: this._renderLoadingContent('We are preparing your download. Depending on the size, it could take some time.'),
formats: this.formats,
url: this.baseUrl,
isGeoreferenced: isGeoreferenced,
hasBounds: hasBounds
}));
} else {
this.$el.html(this._renderLoadingContent('Checking georeferences…'));
}
return this;
}
});
@@ -0,0 +1,148 @@
const Backbone = require('backbone');
const $ = require('jquery');
const carto = require('@carto/carto.js');
const L = require('leaflet');
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./map-view.tpl');
const { simple: styles } = require('builder/data/default-cartography.json');
const REQUIRED_OPTS = [
'username',
'serverUrl',
'sqlUrl',
'vizjson',
'dataset'
];
module.exports = CoreView.extend({
events: {
'click .js-bounds': '_changeBounds'
},
className: 'map',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this.model.on('change:bounds', this._boundsClick, this);
},
_changeBounds: function () {
this.model.set('bounds', !this.model.get('bounds'));
},
_boundsClick: function () {
this.trigger('boundsChanged', { bounds: this.model.get('bounds') });
this.$('.js-bounds .Checkbox-input').toggleClass('is-checked', !!this.model.get('bounds'));
},
render: function () {
this.$el.html(template());
const mapEl = this.$el.find('.carto-map')[0];
this.map = L.map(mapEl).setView([0, 0], 0);
this.map.on('zoomend moveend', () => {
this.trigger('mapBoundsChanged', {
bounds: this.map.getBounds(),
center: this.map.getCenter(),
zoom: this.map.getZoom()
});
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png', {
maxZoom: 18
}).addTo(this.map);
this.client = new carto.Client({
apiKey: 'default_public',
serverUrl: this._serverUrl,
username: this._username
});
this.dataset = new carto.source.Dataset(this._dataset);
const cartocss = new carto.style.CartoCSS('#layer {}');
this.cartoLayer = new carto.layer.Layer(this.dataset, cartocss);
this.client.addLayer(this.cartoLayer);
this.client.getLeafletLayer().addTo(this.map);
this._fetchBounds();
return this;
},
enableMap: function () {
this.map.invalidateSize();
},
setGeometry: function ([geom]) {
let style;
switch (geom) {
case 'ST_Linestring':
style = `#layer {
line-color: ${styles.line.stroke.color.fixed};
line-width: ${styles.line.stroke.size.fixed};
line-opacity: ${styles.line.stroke.color.opacity};
}
`;
break;
case 'ST_Point':
style = `#layer {
marker-width: ${styles.point.fill.size.fixed};
marker-fill: ${styles.point.fill.color.fixed};
marker-fill-opacity: ${styles.point.fill.color.opacity};
marker-line-color: ${styles.point.stroke.color.fixed};
marker-line-width: ${styles.point.stroke.size.fixed};
marker-line-opacity: ${styles.point.stroke.color.opacity};
marker-placement: point;
marker-type: ellipse;
marker-allow-overlap: true;
}`;
break;
case 'ST_Polygon':
style = `#layer {
polygon-fill: ${styles.polygon.fill.color.fixed};
polygon-opacity: ${styles.polygon.fill.color.opacity};
::outline {
line-color: ${styles.polygon.stroke.color.fixed};
line-width: ${styles.polygon.stroke.size.fixed};
line-opacity: ${styles.polygon.stroke.color.opacity};
}
}`;
break;
default:
style = `#layer {}`;
break;
}
this.cartoLayer.setStyle(new carto.style.CartoCSS(style));
},
_fetchBounds: function () {
$.ajax({
type: 'GET',
data: 'q=' + encodeURIComponent(`SELECT ST_Extent(the_geom) as extent FROM ${this._dataset}`) + '&api_key=default_public',
url: this._sqlUrl,
success: (data) => {
const bounds = data.rows[0].extent;
const parsedBounds = /BOX\((.+) (.+),(.+) (.+)\)/.exec(bounds)
.splice(1, 4)
.map(e => parseFloat(e));
this.map.fitBounds([
[parsedBounds[1], parsedBounds[0]], [parsedBounds[3], parsedBounds[2]]
]);
},
error: () => {}
});
},
invalidateMap: function () {
this.map.invalidateSize();
}
});
@@ -0,0 +1,7 @@
<div class="Checkbox js-bounds">
<button class="Checkbox-input"></button>
<label class="Checkbox-label">Match rows with the map view.</label>
</div>
<div class="cartodb-map public">
<div class="carto-map" style="height: 100%"></div>
</div>
@@ -0,0 +1,25 @@
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
const CartoTableData = require('dashboard/data/table/carto-table-data');
module.exports = CartoTableMetadata.extend({
fetch: function () {
this.trigger('sync');
// nothing to fetch here
},
data: function () {
if (this._data === undefined) {
this._data = new CartoTableData(null, {
table: this,
configModel: this._configModel
});
this._data.fetch = function () { };
}
if (this.sqlView) {
return this.sqlView;
}
return this._data;
}
});
@@ -0,0 +1,24 @@
const _ = require('underscore');
const DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
const PublicDropdown = DropdownAdminView.extend({
_TEMPLATE: `
<ul>
<li><a class="small" href="<%- urls[0] %>">View your datasets</a></li>
<li><a class="small" href="<%- urls[0] %>/visualizations">View your maps</a></li>
<li><a class="small" href="<%- urls[0].replace("dashboard", "logout") %>">Close session</a></li>
</ul>
`,
render: function () {
this.$el
.html(_.template(this._TEMPLATE)(this.model.attributes))
.css({
width: this.options.width
});
return this;
}
});
module.exports = PublicDropdown;
@@ -0,0 +1,53 @@
const _ = require('underscore');
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const PublicDropdown = require('dashboard/views/public-dataset/public-dropdown');
const template = require('./public-header.tpl');
/**
* Public header, dance starts!
*
*/
module.exports = CoreView.extend({
initialize: function () {
this.vis = this.options.vis;
this.template = template;
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.html(
this.template(
_.defaults({
vis_url: this.vis && this.vis.viewUrl(this.model) || '',
isMobileDevice: this.options.isMobileDevice,
owner_username: this.options.owner_username,
current_view: this.options.current_view,
isCartoDBHosted: this.options.isCartoDBHosted
}, this.model.attributes)
)
);
this._initViews();
return this;
},
_initBinds: function () {
this.model.bind('change', this.render, this);
},
_initViews: function () {
if (this.$('.account').length > 0) {
var dropdown = new PublicDropdown({
target: this.$('a.account'),
model: this.model,
vertical_offset: 20,
width: 166
});
this.addView(dropdown);
$('body').append(dropdown.render().el);
}
}
});
@@ -0,0 +1,21 @@
<div class='inner'>
<h1><a href='https://carto.com' class='logo' id='the_logo'>CARTO</a></h1>
<ul class='options'>
<% if (!username && !isMobileDevice) { %>
<li><a href='https://carto.com/learn/guides' class='guides'>Guides</a></li>
<% } %>
<% if (!username) { %>
<% if ( !isCartoDBHosted ) { %>
<li><a class='signup' href='https://carto.com/signup'>Sign up</a><li>
<% } %>
<li><a href='https://carto.com/login' class='border login'>Login</a></li>
<% } else { %>
<li>
<a class='editor dropdown account' href='<%- urls[0] %>'>
<% if (avatar_url) { %><img src='<%- avatar_url %>' width='18' title='<%- username %>' alt='<%- username %>' /><% } %><%- username %><span class='separator'></span>
</a>
</li>
<% } %>
</ul>
</div>
@@ -0,0 +1,7 @@
<div class="emptyTableContainer">
<div class="emptyPublicTableInfo emptynessInfo">
<h4>This dataset is empty</h4>
<p>If you are the owner of this dataset remember to login and add your desired rows.</p>
<p>If not, you can’t do anything interesting here.</p>
</div>
</div>
@@ -0,0 +1,37 @@
const CoreView = require('backbone/core-view');
const template = require('./public-table-header-view.tpl');
// On the original code, this extended a more complex base class, but the public table needs no functionality
const HeaderView = CoreView.extend({
events: {
},
initialize: function () {
this.column = this.options.column;
this.table = this.options.table;
this.editing_name = false;
this.changing_type = false;
},
render: function () {
this.$el.html('');
this.$el.append(template({
col_name: this.column[0],
col_type: this.column[1],
editing_name: this.editing_name
}));
// Focus in the input if it is being edited
if (this.editing_name) {
this.$el.find('input').focus();
}
this.delegateEvents();
return this;
}
});
module.exports = HeaderView;
@@ -0,0 +1,24 @@
const RowView = require('dashboard/views/public-dataset/table-view/row-view');
/**
* view used to render each row in public tables
*/
module.exports = RowView.extend({
classLabel: 'cdb.open.PublicRowView',
events: {},
initialize: function () {
this.options.row_header = false;
this.order = this.options.order;
},
_renderGeometry: function (value) {
return this._renderDefault('GeoJSON');
},
_getRowOptions: function () {},
click_header: function (e) {}
});
@@ -0,0 +1,14 @@
<div>
<label class="strong small interactuable">
<div class="coloptions" href="#<%- col_name %>">
<%- col_name %>
<% if (col_type == 'geometry') { %>
<span class="tiny geo">GEO</span>
<% } %>
</div>
</label>
<p class="small">
<a class="disabled"><%- col_type %></a>
</p>
</div>
<p class="auto"><%- col_name %>_<% if (col_type == 'geometry') { %>GEO<% } %></p>
@@ -0,0 +1,89 @@
const _ = require('underscore');
const $ = require('jquery');
const TableView = require('dashboard/views/public-dataset/table-view/table-view');
const PublicRowView = require('dashboard/views/public-dataset/public-table-view/public-row-view');
const PublicHeaderView = require('dashboard/views/public-dataset/public-table-view/public-header-view');
const templateEmptyDataset = require('./empty-table-public.tpl');
module.exports = TableView.extend({
events: {},
rowView: PublicRowView,
initialize: function (opts) {
TableView.prototype.initialize.call(this, opts);
this.options.row_header = true;
this._editorsOpened = null;
this.initializeBindings();
this.initPaginationAndScroll();
},
initializeBindings: function () {
_.bindAll(this, 'render', 'rowSaving', 'addEmptyRow',
'_checkEmptyTable', '_forceScroll', '_scrollMagic',
'rowChanged', 'rowSynched', '_startPagination', '_finishPagination',
'rowFailed', 'rowDestroyed', 'emptyTable');
this.model.data().bind('newPage', this.newPage, this);
// this.model.data().bind('loadingRows', this._startPagination);
this.model.data().bind('endLoadingRows', this._finishPagination);
this.bind('cellDblClick', this._editCell, this);
// this.model.bind('change:dataSource', this._onSQLView, this);
// when model changes the header is re rendered so the notice should be added
// this.model.bind('change', this._onSQLView, this);
this.model.bind('dataLoaded', () => {
this._checkEmptyTable();
this._forceScroll();
}, this);
},
headerView: function (column) {
if (column[1] !== 'header') {
var v = new PublicHeaderView({
column: column,
table: this.model,
sqlView: this.options.sqlView
});
this.addView(v);
return v.render().el;
} else {
return '<div><div></div></div>';
}
},
_onSQLView: function () {},
_checkEmptyTable: function () {
if (this.isEmptyTable()) {
this.addEmptyTableInfo();
} else {
this.cleanEmptyTableInfo();
this.$('footer').remove();
}
},
_swicthEnabled: function () {
// this check is not needed in public table
},
initPaginationAndScroll: function () {
// disable scroll pagination, it's firing a function each 300 ms and it's not required on public table
},
addEmptyTableInfo: function () {
var content = templateEmptyDataset(this.import_);
var $footer = $('<tfoot><tr><td colspan="100">' + content + '</td></tr></tfoot>');
this.$('footer').remove();
this.$el.append($footer);
},
_scrollMagic: function () { }
});
@@ -0,0 +1,139 @@
/* global DISQUS */
const Backbone = require('backbone');
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const $ = require('jquery');
const TablePublicView = require('dashboard/views/public-dataset/table-public-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
/**
* Public table window 'view'
*
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.$body = $(this.el.document.body);
this.$map = this.$body.find('#map');
this._initBinds();
this._initViews();
setTimeout(this._onStart, 250);
},
_initViews: function () {
// Table view
var table_options = _.defaults({ el: this.el.document.body, model: new Backbone.Model({ bounds: false, map: null }) }, this.options);
this.table = new TablePublicView(table_options);
const activeTab = (window.location.hash || '').split('/')[1];
this._setActiveTab(activeTab);
},
_initBinds: function () {
_.bindAll(this, '_onWindowResize', '_onOrientationChange', '_onStart');
this.$el.on('resize', this._onWindowResize);
if (!this.el.addEventListener) {
this.el.attachEvent('orientationchange', this._onOrientationChange, this);
} else {
this.el.addEventListener('orientationchange', this._onOrientationChange);
}
this.$body.find('.navigation a[data-pane]').click(this._onNavigationClick.bind(this));
},
// On start view!
_onStart: function () {
this._setupMapDimensions();
},
_onWindowResize: function () {
// Resize window
this._setupMapDimensions();
},
_onOrientationChange: function () {
// Reset disqus
DISQUS && DISQUS.reset({ reload: true });
// Resize window orientation
this._setupMapDimensions(true);
},
_onNavigationClick: function (event) {
const clickedElement = event.currentTarget;
const paneToShow = clickedElement.getAttribute('data-pane');
this._setActiveTab(paneToShow);
if (this.table) this.table.invalidateMap();
},
_setActiveTab: function (tab) {
if (tab === 'table') {
this.table.showTable();
} else if (tab === 'map') {
this.table.showMap();
}
},
// When window is resized, let's touch some things ;)
_setupMapDimensions: function (animated) {
var windowHeight = this.$el.height();
var mainInfoHeight = this.$body.find('.js-Navmenu').height();
var headerHeight = this.$body.find('.Header').height();
var landscapeMode = this.el.matchMedia && this.el.matchMedia('(orientation: landscape)').matches;
var h, height;
if (this.options.isMobileDevice) {
if (landscapeMode) {
h = headerHeight + 7;
} else {
if (windowHeight > 670) {
h = 220;
} else { // iPhone, etc.
h = 138;
}
}
} else {
h = 260;
}
height = windowHeight - h;
if (animated) {
this.$map.animate({ height: height }, { easing: 'easeInQuad', duration: 150 });
} else {
if (this.options.isMobileDevice) {
this.$map.css({ height: height, opacity: 1 });
} else {
// On non mobile devices
this.$map.css({ height: windowHeight - (mainInfoHeight + headerHeight), opacity: 1 });
}
}
// If landscape, let's scroll to show the map, and
// leave the header hidden
if (this.options.isMobileDevice && landscapeMode && $(window).scrollTop() < headerHeight) {
this.$body.animate({ scrollTop: headerHeight }, 600);
}
if (this.table) this.table.invalidateMap();
this._showNavigationBar();
},
// Show navigation (table or map view) block
_showNavigationBar: function () {
this.$body.find('.navigation')
.animate({ opacity: 1 }, 250);
if (this.table) this.table.invalidateMap();
}
});
@@ -0,0 +1,294 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const UserModel = require('dashboard/data/user-model');
const AuthenticatedUserModel = require('dashboard/data/authenticated-user-model');
const SQLViewData = require('dashboard/data/table/sqlviewdata-model');
const PublicCartoTableMetadata = require('./public-carto-table-metadata');
const PublicHeader = require('./public-header/public-header');
const PublicTableTab = require('dashboard/views/public-dataset/table-tab/public-table-tab');
const TabPane = require('dashboard/components/tabpane/tabpane');
const MapView = require('dashboard/views/public-dataset/map-view/map-view');
const Tabs = require('dashboard/components/tabs/tabs');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const ViewFactory = require('builder/components/view-factory');
const templateApiDialog = require('dashboard/views/public-dataset/dialogs/api-call.tpl');
const PublicExportView = require('dashboard/views/public-dataset/dialogs/export/public-export-view');
const REQUIRED_OPTS = [
'configModel'
];
/**
* Table public view
*
*/
module.exports = CoreView.extend({
events: {
'click .js-Navmenu-link--download': '_exportTable',
'click .js-Navmenu-link--api': '_apiCallTable'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initModels();
this._initViews();
this._initBinds();
},
_initModels: function () {
this._modals = new ModalsServiceModel();
// Table
this.table = new PublicCartoTableMetadata({
id: this.options.table_name,
name: this.options.table_name,
description: this.options.vizjson.description || ''
}, { configModel: this._configModel });
this.table.set({
user_name: this.options.user_name,
vizjson: this.options.vizjson,
schema: this.options.schema
});
this.columns = this.table.data();
this.sqlView = new SQLViewData(null, {
configModel: this._configModel
});
this.sqlView.syncMethod = 'read';
var query = this.query = this.table.data().getSQL();
this.table.useSQLView(this.sqlView);
this.sqlView.options.set('rows_per_page', 20, { silent: true });
this._fetchData(query);
// User
this.user = new UserModel({ username: this.options.user_name });
// Authenticated user
this.authenticated_user = new AuthenticatedUserModel();
},
_initViews: function () {
// Public header
if (this.$('.cartodb-public-header').length > 0) {
var header = new PublicHeader({
el: this.$('.cartodb-public-header'),
model: this.authenticated_user,
vis: this.table,
current_view: this._getCurrentView(),
owner_username: this.options.owner_username,
isMobileDevice: this.options.isMobileDevice,
isCartoDBHosted: this._configModel.get('cartodb_com_hosted')
});
this.addView(header);
// Fetch authenticated user model
this.authenticated_user.fetch();
}
// Navigation - This is what switches the view on mobile
// TODO: this is insanely complex for just two buttons
// this.header = new cdb.open.PublicHeader({
// el: this.$('.navigation'),
// model: this.table,
// user: this.user,
// belong_organization: belong_organization,
// config: this.options.config
// });
// this.addView(this.header);
// Tabpanes
this.workViewTable = new TabPane({
el: this.$('.pane_table')
});
this.addView(this.workViewTable);
this.workViewMap = new TabPane({
el: this.$('.pane_map')
});
this.addView(this.workViewMap);
// Public app tabs
this.tabs = new Tabs({
el: this.$('.navigation ul'),
slash: true
});
this.addView(this.tabs);
// Help tooltip - I can't find any span with help class, I think this is unnecessary
// var tooltip = new cdb.common.TipsyTooltip({
// el: this.$('span.help'),
// gravity: $.fn.tipsy.autoBounds(250, 's')
// });
// this.addView(tooltip);
// Table tab
this.tableTab = new PublicTableTab({
configModel: this._configModel,
model: this.table,
vizjson: this.options.vizjson,
user_name: this.options.user_name
});
// Map tab
this.mapTab = new MapView({
username: this.options.user_name,
serverUrl: this._configModel.get('maps_api_template').replace('{user}', this.options.user_name),
sqlUrl: this._configModel.getSqlApiUrl(),
dataset: this.table.get('name'),
vizjson: this.table.get('vizjson'),
geometry: this.table.get('geometry_types')
});
this.listenTo(this.mapTab, 'mapBoundsChanged', function (options) {
const {
bounds,
center,
zoom
} = options;
this.model.set('map', {
bounds: [
bounds.getNorthEast().lng,
bounds.getNorthEast().lat,
bounds.getSouthWest().lng,
bounds.getSouthWest().lat
],
center,
zoom
});
});
this.listenTo(this.mapTab, 'boundsChanged', function (options) {
this.model.set('bounds', options.bounds);
});
this.workViewTable.addTab('table', this.tableTab.render());
this.workViewMap.addTab('map', this.mapTab.render());
this.workViewTable.active('table');
this.workViewMap.active('map');
this.mapTab.enableMap();
$('.pane_table').addClass('is-active');
},
_updateTable: function () {
var sql = (this.model.get('bounds') && this.model.get('map')) ? (this.query + ' WHERE the_geom && ST_MakeEnvelope(' + this.model.get('map')['bounds'][0] + ', ' + this.model.get('map')['bounds'][1] + ', ' + this.model.get('map')['bounds'][2] + ', ' + this.model.get('map')['bounds'][3] + ', 4326)') : this.query;
this._fetchData(sql);
},
_fetchData: function (sql) {
if (sql) {
this.sqlView.setSQL(sql);
}
this.sqlView.fetch({
success: () => {
this.$('.js-spinner').remove();
this.tableTab.deactivated();
this.tableTab.activated();
}
});
},
_exportTable: function (e) {
e.preventDefault();
// If a sql is applied but it is not valid, don't let the user export it
if (!this.sqlView.getSQL()) return false;
this._modals.create((modalModel) =>
new PublicExportView({
modalModel,
configModel: this._configModel,
model: this.table,
user_data: this.user.toJSON(),
bounds: this.sqlView.getSQL() !== this.query
})
);
},
_apiCallTable: function (e) {
e.preventDefault();
// If a sql is applied but it is not valid, don't show the dialog
if (!this.sqlView.getSQL()) return false;
this._modals.create(() =>
ViewFactory.createByTemplate(
templateApiDialog,
{
url: this._configModel.getSqlApiUrl(),
sql: this.sqlView.getSQL(),
schema: this.table.attributes.original_schema.slice(0, 5),
rows: this.table.dataModel.models
}
)
);
},
_initBinds: function () {
this.listenTo(this.model, 'change:bounds', function (model) {
this._updateTable();
});
this.listenTo(this.model, 'change:map', function (model) {
if (model.get('bounds')) {
this._updateTable();
}
});
this.authenticated_user.bind('change', this._onUserLogged, this);
this.add_related_model(this.authenticated_user);
this.listenToOnce(this.table, 'change:geometry_types', function (model) {
this.mapTab.setGeometry(model.get('geometry_types'));
});
},
// Get type of current view
// - It could be, dashboard, table or visualization
_getCurrentView: function () {
var pathname = location.pathname;
if (pathname.indexOf('/tables/') !== -1) {
return 'table';
}
if (pathname.indexOf('/viz/') !== -1) {
return 'visualization';
}
// Other case -> dashboard (datasets, visualizations,...)
return 'dashboard';
},
keyUp: function (e) {},
_onUserLogged: function () {
// Check if edit button should be visible
if (this.options.owner_username === this.authenticated_user.get('username')) {
this.$('.extra_options .edit').css('display', 'inline-block');
this.$('.extra_options .oneclick').css('display', 'none');
}
},
invalidateMap: function () {
this.mapTab.invalidateMap();
},
showTable: function () {
$('.pane_table').addClass('is-active');
$('.pane_map').removeClass('is-active');
this.tabs.activate('table');
},
showMap: function () {
$('.pane_table').removeClass('is-active');
$('.pane_map').addClass('is-active');
this.tabs.activate('map');
}
});
@@ -0,0 +1,27 @@
const TableTab = require('dashboard/views/public-dataset/table-tab/table-tab');
const PublicTableView = require('dashboard/views/public-dataset/public-table-view/public-table-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = TableTab.extend({
className: 'table public',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.user = this.options.user;
this.sqlView = this.options.sqlView;
},
_createTable: function () {
this.tableView = new PublicTableView({
configModel: this._configModel,
dataModel: this.model.data(),
model: this.model,
sqlView: this.sqlView
});
}
});
@@ -0,0 +1,65 @@
const CoreView = require('backbone/core-view');
module.exports = CoreView.extend({
className: 'table',
initialize: function () {
this.user = this.options.user;
this.sqlView = this.options.sqlView;
this.geocoder = this.options.geocoder;
this.backgroundPollingModel = this.options.backgroundPollingModel;
this._initBinds();
},
setActiveLayer: function (layerView) {
var recreate = !!this.tableView;
this.deactivated();
this.model = layerView.table;
this.layer = layerView.model;
this.sqlView = layerView.sqlView;
if (recreate) {
this.activated();
}
},
_initBinds: function () {
// Geocoder binding
this.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function () {
if (this.model.data) {
this.model.data().refresh();
}
}, this);
this.add_related_model(this.geocoder);
},
_createTable: function () {
throw new Error('Method not migrated, check original implementation');
},
activated: function () {
if (!this.tableView) {
this._createTable();
this.tableView.render();
this.render();
}
},
deactivated: function () {
if (this.tableView) {
this.tableView.clean();
this.tableView = null;
this.hasRenderedTableView = false;
}
},
render: function () {
// Since render should be idempotent (i.e. should not append the tableView twice when called multiple times)
if (this.tableView && !this.hasRenderedTableView) {
this.hasRenderedTableView = true;
this.$el.append(this.tableView.el);
}
return this;
}
});
@@ -0,0 +1,6 @@
<div class="emptyTableContainer">
<div class="emptySQLInfo emptynessInfo">
<h4 class="strong">No results on this query</h4>
<p>Try with a different one or just <a href="#" class="clearview">clear this view</a></p>
</div>
</div>
@@ -0,0 +1,135 @@
const _ = require('underscore');
const $ = require('jquery');
const RowView = require('dashboard/components/table/row-view');
const RowModel = require('dashboard/data/table/row-model');
/**
* view used to render each row
*/
module.exports = RowView.extend({
classLabel: 'cdb.admin.RowView',
events: {
'click .row_header': '_onOptionsClick',
'mouseout .row_header': '_onOptionsOut'
},
cellRenderers: {
'default': '_renderDefault',
'boolean': '_renderBoolean',
'number': '_renderNumber',
'geometry': '_renderGeometry'
},
initialize: function (opts) {
RowView.prototype.initialize.call(this, opts);
_.bindAll(this, '_onOptionsClick', '_onOptionsOut');
this.options.row_header = true;
this.retrigger('saving', this.model);
this.retrigger('saved', this.model);
this.retrigger('errorSaving', this.model);
},
_getRowOptions: function () {
throw new Error('Method not migrated, check original implementation');
},
_onOptionsOut: function (e) {
var $relatedTarget = $(e.relatedTarget);
if ($relatedTarget.closest('.dropdown').length == 0 && $relatedTarget.closest('.row_header').length == 0) { // eslint-disable-line eqeqeq
if (this.rowOptions) this.rowOptions.hide();
}
},
_onOptionsClick: function (e) {
var rowOptions;
var left;
var top;
var $target = $(e.target);
if (this.table.isReadOnly()) {
// if data is read only do not allow
// to add or remove rows
return;
}
rowOptions = this._getRowOptions();
e.preventDefault();
$target.append(rowOptions.el);
left = 30;
top = 5;
// If it is the last td of the table, show the dropdown upwards
if ($target.closest('tr').is(':last-child') && $target.closest('tr').index() > 1) {
rowOptions.options.vertical_position = 'top';
rowOptions.options.tick = 'bottom';
} else {
rowOptions.options.vertical_position = 'down';
rowOptions.options.tick = 'top';
}
rowOptions.setRow(this.model);
rowOptions.openAt(left, top);
this.rowOptions = rowOptions; // to make it testable;
return false;
},
/**
* return each cell view
*/
valueView: function (colName, value) {
this.table = this.table || this.getTableView().model;
var render = '_renderDefault';
if (colName.length) {
var colType = this.table.getColumnType(colName);
render = this.cellRenderers[colType] || render;
}
var obj = $(this[render](value));
obj.addClass('cell');
if (RowModel.isReservedColumn(colName)) {
obj.addClass('disabled');
}
// It is the first cell?
if (colName === '' && value === '') {
if (this.table.isReadOnly()) {
// No row options button
obj
.addClass('disabled')
.html('');
} else {
// Add row options button
obj.addClass('row_header');
}
}
return obj;
},
_renderDefault: function (value, additionalClasses) {
additionalClasses = additionalClasses || '';
var cell;
if (value === null) {
additionalClasses += ' isNull';
cell = '<div class="' + additionalClasses + '">null</div>';
} else cell = '<div class="' + additionalClasses + '">' + _.escape(value) + '</div>';
return cell;
},
_renderBoolean: function (value) {
return this._renderDefault(value, 'boolean');
},
_renderNumber: function (value) {
return this._renderDefault(value, 'number');
},
_renderGeometry: function (value) {
// Overriden in public-row-view
}
});
@@ -0,0 +1,8 @@
<div class="float_info loading <%- panel_opened ? 'displaced' : '' %>">
<div class="CDB-LoaderIcon CDB-LoaderIcon--big is-blue">
<svg class="CDB-LoaderIcon-spinner" viewbox="0 0 50 50">
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"/>
</svg>
</div>
<h5 class="strong">Loading results</h5>
</div>
@@ -0,0 +1,12 @@
<div class="sqlview">
<p class="displaced <% if(warnMsg) print("warn") %>">
<% if (!empty) { %>
<% if (warnMsg) { %>
<%- warnMsg %> ·
<% } %>
<a href="#/export-query" class="export_query">create dataset from query</a> or <a href="#/clear-view" class="clearview">clear view</a>
<% } else { %>
No resulting rows for this query, <a href="#/clear-view" class="clearview">clear this sql view</a>
<% } %>
</p>
</div>
@@ -0,0 +1,15 @@
<tfoot class='page_loader <%- direction %>'>
<tr>
<th colspan='1'>
<div class='fake'></div>
<div class='float_info'>
<h5>Scroll <%- direction == "up" ? 'down' : 'up' %> and then <%- direction %></h5>
<p>It will fire the pagination.</p>
</div>
<div class='float_action'>
<h5>Loading more rows...</h5>
<p>Just a few seconds.</p>
</div>
</th>
</tr>
</tfoot>
@@ -0,0 +1,539 @@
const $ = require('jquery');
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const Table = require('dashboard/components/table/table');
const templateSQLViewNotice = require('./sql-view-notice.tpl');
const templateSQLLoading = require('./sql-loading.tpl');
const templateEmptySQL = require('./empty-sql.tpl');
const templateTablePaginationLoaders = require('./table-pagination-loaders.tpl');
const RowView = require('./row-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = Table.extend({
classLabel: 'cdb.admin.TableView',
events: CoreView.extendEvents({
'click .clearview': '_clearView',
'click .sqlview .export_query': '_tableFromQuery',
'click .noRows': 'addEmptyRow'
}),
rowView: RowView,
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
Table.prototype.initialize.call(this);
this.options.row_header = true;
this.globalError = this.options.globalError;
this.vis = this.options.vis;
this.user = this.options.user;
this._editorsOpened = null;
this.initializeBindings();
this.initPaginationAndScroll();
},
/**
* Append all the bindings needed for this view
* @return undefined
*/
initializeBindings: function () {
_.bindAll(this, 'render', 'rowSaving', 'addEmptyRow',
'_checkEmptyTable', '_forceScroll', '_scrollMagic',
'rowChanged', 'rowSynched', '_startPagination', '_finishPagination',
'rowFailed', 'rowDestroyed', 'emptyTable');
this.model.data().bind('newPage', this.newPage, this);
// this.model.data().bind('loadingRows', this._startPagination);
this.model.data().bind('endLoadingRows', this._finishPagination);
this.bind('cellDblClick', this._editCell, this);
this.bind('createRow', () => {
this._checkEmptyTable();
});
this.model.bind('change:dataSource', this._onSQLView, this);
// when model changes the header is re rendered so the notice should be added
// this.model.bind('change', this._onSQLView, this);
this.model.bind('dataLoaded', () => {
// this._checkEmptyTable();
this._forceScroll();
}, this);
this.model.bind('change:permission', this._checkEmptyTable, this);
this.model.bind('change:isSync', this._swicthEnabled, this);
this._swicthEnabled();
// Geocoder binding
this.options.geocoder.bind('geocodingComplete geocodingError geocodingCanceled', function () {
this.notice(_t('loaded'));
}, this);
this.add_related_model(this.options.geocoder);
},
initPaginationAndScroll: function () {
var topReached = false;
var bottomReached = false;
// Initialize moving header and loaders when scrolls
this.scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop(), last: 'vertical' };
$(window).scroll(this._scrollMagic);
// Pagination
var SCROLL_BACK_PIXELS = 2;
this.checkScrollTimer = setInterval(() => {
if (!this.$el.is(':visible') || this.model.data().isFetchingPage()) {
return;
}
var pos = $(this).scrollTop();
var d = this.model.data();
// do not let to fetch previous pages
// until the user dont scroll back a little bit
// see comments below
if (pos > SCROLL_BACK_PIXELS) {
topReached = false;
}
var pageSize = $(window).height() - this.$el.offset().top;
var tableHeight = this.$('tbody').height();
var realPos = pos + pageSize;
if (tableHeight < pageSize) {
return;
}
// do not let to fetch previous pages
// until the user dont scroll back a little bit
// if we dont do this when the user reach the end of the page
// and there are more rows than max_rows, the rows form the beggining
// are removed and the scroll keeps at the bottom so a new page is loaded
// doing this the user have to move the scroll a little bit (2 px)
// in order to load the page again
if (realPos < tableHeight - SCROLL_BACK_PIXELS) {
bottomReached = false;
}
if (realPos >= tableHeight) {
if (!bottomReached) {
// Simulating loadingRows event
if (!d.lastPage) this._startPagination('down');
setTimeout(function () {
d.loadPageAtBottom();
}, 600);
}
bottomReached = true;
} else if (pos <= 0) {
if (!topReached) {
// Simulating loadingRows event
if (d.pages && d.pages[0] != 0) this._startPagination('up'); // eslint-disable-line eqeqeq
setTimeout(function () {
d.loadPageAtTop();
}, 600);
}
topReached = true;
}
this._setUpPagination(d);
}, 300);
this.bind('clean', function () {
clearInterval(this.checkScrollTimer);
}, this);
},
needsRender: function (table) {
if (!table) return true;
var ca = table.changedAttributes();
if (ca.geometry_types && _.keys(ca).length === 1) {
return false;
}
return true;
},
render: function (args) {
if (!this.needsRender(args)) return;
Table.prototype.render.call(this, args);
if (this.model.isInSQLView()) {
this._onSQLView();
}
this._swicthEnabled();
this.trigger('render');
},
_renderHeader: function () {
var thead = Table.prototype._renderHeader.apply(this);
// New custom shadow (better performance)
thead.append($('<div>').addClass('shadow'));
return thead;
},
addColumn: function (column) {
this.newColumnName = 'column_' + new Date().getTime();
this.model.addColumn(this.newColumnName, 'string');
this.unbind('render', this._highlightColumn, this);
this.bind('render', this._highlightColumn, this);
},
_highlightColumn: function () {
if (this.newColumnName) {
var $th = this.$("a[href='#" + this.newColumnName + "']").parents('th');
var position = $th.index();
if (position) {
setTimeout(function () {
var windowWidth = $(window).width();
if ($th && $th.position()) {
var centerPosition = $th.position().left - windowWidth / 2 + $th.width() / 2;
$(window).scrollLeft(centerPosition);
}
this.$("[data-x='" + position + "']").addClass('is-highlighted');
}, 300);
this.unbind('render', this._highlightColumn, this);
}
}
},
/**
* Take care if the table needs space at top and bottom
* to show the loaders.
*/
_setUpPagination: function (d) {
var pages = d.pages;
// Check if the table is not in the first page
if (pages.length > 0 && pages[0] > 0) {
// In that case, add the paginator-up loader and leave it ready
// when it is necessary
if (this.$el.find('tfoot.page_loader.up').length == 0) { // eslint-disable-line eqeqeq
this.$el.append(templateTablePaginationLoaders({ direction: 'up' }));
}
// Table now needs some space at the top to show the loader
this.$el.parent().addClass('page_up');
} else {
// Loader is not needed and table doesn't need any space at the top
this.$el.parent().removeClass('page_up');
}
// Checks if we are in the last page
if (!d.lastPage) {
// If not, let's prepare the paginator-down
if (this.$el.find('tfoot.page_loader.down').length == 0) { // eslint-disable-line eqeqeq
this.$el.append(templateTablePaginationLoaders({ direction: 'down' }));
}
// Let's say to the table that we have paginator-down
this.$el.parent().addClass('page_down');
} else {
// Loader is not needed and table doesn't need any space at the bottom
this.$el.parent().removeClass('page_down');
}
},
/**
* What to do when a pagination starts
*/
_startPagination: function (updown) {
// Loader... move on buddy!
this.$el.find('.page_loader.' + updown + '').addClass('active');
},
/**
* What to do when a pagination finishes
*/
_finishPagination: function (page, updown) {
// If we are in a different page than 0, and we are paginating up
// let's move a little bit the scroll to hide the loader again
// HACKY
if (page != 0 && updown == 'up') { // eslint-disable-line eqeqeq
setTimeout(function () {
$(window).scrollTop(180);
}, 300);
}
this.$el.find('.page_loader.active').removeClass('active');
},
_onSQLView: function () {
// bind each time we change dataSource because table unbind
// all the events from sqlView object each time useSQLView is called
this.$('.sqlview').remove();
this.options.sqlView.unbind('reset error', this._renderSQLHeader, this);
this.options.sqlView.unbind('loading', this._renderLoading, this);
this.options.sqlView.bind('loading', this._renderLoading, this);
this.options.sqlView.bind('reset', this._renderSQLHeader, this);
this.options.sqlView.bind('error', this._renderSQLHeader, this);
this._renderSQLHeader();
},
_renderLoading: function (opts) {
opts = opts || {};
this.cleanEmptyTableInfo();
if (!opts.add) {
this._renderBodyTemplate(templateSQLLoading);
}
},
_renderSQLHeader: function () {
if (this.model.isInSQLView()) {
var empty = this.isEmptyTable();
this.$('thead').find('.sqlview').remove();
this.$('thead').append(
templateSQLViewNotice({
empty: empty,
isVisualization: this.vis.isVisualization(),
warnMsg: null
})
);
this.$('thead > tr').css('height', 64 + 42);
if (this.isEmptyTable()) {
this.addEmptySQLIfo();
}
this._moveInfo();
}
},
// depending if the sync is enabled add or remove a class
_swicthEnabled: function () {
// Synced?
this.$el[ this.model.isSync() ? 'addClass' : 'removeClass' ]('synced');
// Visualization?
this.$el[ this.vis.isVisualization() ? 'addClass' : 'removeClass' ]('vis');
},
_clearView: function (e) {
if (e) e.preventDefault();
this.options.layer.clearSQLView();
return false;
},
_tableFromQuery: function (e) {
throw new Error('Method not migrated, check original implementation');
},
/**
* Function to control the scroll in the table (horizontal and vertical)
*/
_scrollMagic: function (ev) {
var actual_scroll_position = { x: $(window).scrollLeft(), y: $(window).scrollTop() };
if (this.scroll_position.x != actual_scroll_position.x) { // eslint-disable-line eqeqeq
this.scroll_position.x = actual_scroll_position.x;
this.$el.find('thead').addClass('horizontal');
// If last change was vertical
if (this.scroll_position.last == 'vertical') { // eslint-disable-line eqeqeq
this.scroll_position.x = actual_scroll_position.x;
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
.removeAttr('style')
.css('top', actual_scroll_position.y + 'px');
this.scroll_position.last = 'horizontal';
}
} else if (this.scroll_position.y != actual_scroll_position.y) { // eslint-disable-line eqeqeq
this.scroll_position.y = actual_scroll_position.y;
this.$el.find('thead').removeClass('horizontal');
// If last change was horizontal
if (this.scroll_position.last == 'horizontal') { // eslint-disable-line eqeqeq
this.$el.find('thead > tr > th > div > div:not(.dropdown)')
.removeAttr('style')
.css({'marginLeft': '-' + actual_scroll_position.x + 'px'});
this.scroll_position.last = 'vertical';
}
}
},
/**
* Move the info content if the panel is opened or hidden.
* - Query info if query is applied
* - Query loader if query is appliying in that moment
* - Add some padding to last column of the content to show them
*/
_moveInfo: function (type) {
if (type == 'show') { // eslint-disable-line eqeqeq
this.$el
.removeClass('narrow')
.addClass('displaced');
} else if (type == 'narrow') { // eslint-disable-line eqeqeq
this.$el.addClass('displaced narrow');
} else if (type == 'hide') { // eslint-disable-line eqeqeq
this.$el.removeClass('displaced narrow');
} else {
// Check from the beginning if the right menu is openned, isOpen from
// the menu is not working properly
if ($('.table_panel').length > 0) {
var opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
if (!opened) {
this.$el.removeClass('displaced');
}
}
}
},
_getEditor: function (columnType, opts) {
throw new Error('Method not migrated, check original implementation');
},
closeEditor: function () {
if (this._editorsOpened) {
this._editorsOpened.hide();
this._editorsOpened.clean();
}
},
_editCell: function (e, cell, x, y) {
throw new Error('Method not migrated, check original implementation');
},
headerView: function (column) {
throw new Error('Method not migrated, check original implementation');
},
/**
* Checks if the table has any rows, and if not, launch the method for showing the appropiate view elements
* @method _checkEmptyTable
*/
_checkEmptyTable: function () {
if (this.isEmptyTable()) {
this.addEmptyTableInfo();
} else {
this.cleanEmptyTableInfo();
}
},
/**
* Force the table to be at the beginning
* @method _forceScroll
*/
_forceScroll: function (ev) {
$(window).scrollLeft(0);
},
_renderEmpty: function () {
this.addEmptyTableInfo();
},
/**
* Adds the view elements associated with no content in the table
* @method addemptyTableInfo
*/
addEmptyTableInfo: function () {
throw new Error('Method not migrated, check original implementation');
},
/**
* Adds the view elements associated with no content in the table when a SQL is applied
* @method addEmptySQLIfo
*/
addEmptySQLIfo: function () {
if (this.model.isInSQLView()) {
this._renderBodyTemplate(templateEmptySQL);
}
},
_renderBodyTemplate: function (template) {
this.$('tbody').html('');
this.$('tfoot').remove();
this.$el.hide();
// Check if panel is opened to move the loader some bit left
var panel_opened = false;
if ($('.table_panel').length > 0) {
panel_opened = $('.table_panel').css('right').replace('px', '') == 0; // eslint-disable-line eqeqeq
}
var content = template({ config: this._configModel, panel_opened });
var $footer = $('<tfoot class="sql_loader"><tr><td colspan="100">' + content + '</td></tr></tfoot>');
this.$el.append($footer);
this.$el.fadeIn();
},
/**
* Removes from the view the no-content elements
* @method cleanEmptyTableInfo
*/
cleanEmptyTableInfo: function () {
this.$('tfoot').fadeOut('fast', function () {
$(this).remove();
});
this.$('.noRows').slideUp('fast', function () {
$(this).remove();
});
},
notice: function (text, type, time) {
this.globalError.showError(text, type, time);
},
/**
* Add a new row and removes the empty table view elemetns
* @method addEmptyRow
* @todo: (xabel) refactor this to include a "addRow" method in _models[0]
*/
addEmptyRow: function () {
this.dataModel.addRow({ at: 0 });
this.cleanEmptyTableInfo();
},
/**
* Captures the saving event from the row and produces a notification
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowSaving: function () {
this.notice('Saving your edit', 'load', -1);
},
/**
* Captures the change event from the row and produces a notification
* @method rowSynched
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowSynched: function () {
this.notice('Sucessfully saved');
},
/**
* Captures the change event from the row and produces a notification
* @method rowSynched
* @todo (xabel) i'm pretty sure there has to be a less convulted way of doing this, without capturing a event
* to throw another event in the model to be captured by some view
*/
rowFailed: function () {
this.notice('Oops, there has been an error saving your changes.', 'error');
},
/**
* Captures the destroy event from the row and produces a notification
* @method rowDestroyed
*/
rowDestroying: function () {
this.notice('Deleting row', 'load', -1);
},
/**
* Captures the sync after a destroy event from the row and produces a notification
* @method rowDestroyed
*/
rowDestroyed: function () {
this.notice('Sucessfully deleted');
this._checkEmptyTable();
}
});