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,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();
}
});