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,8 @@
<ul class="CodeMirror-error">
<li class="CodeMirror-errorMessage u-lSpace--xl u-rSpace--xl">
<%- _t('components.codemirror.syntax-error') %>: <span><%- message %></span>
</li>
<li class="CodeMirror-errorDocs">
<a href="https://carto.com/developers/sql-api/" target="_black"><%- _t('components.codemirror.docs') %></a>
</li>
</ul>
@@ -0,0 +1,28 @@
var _ = require('underscore');
var BLACKLISTED_COLUMNS = ['created_at', 'the_geom', 'the_geom_webmercator', 'updated_at'];
module.exports = function (nodeDefModel, layerDefinitionModel) {
var tuplesItems = {};
if (!layerDefinitionModel) return tuplesItems; // e.g. if the node is one of those temporary tables
var querySchemaModel = nodeDefModel.querySchemaModel;
if (!querySchemaModel) return tuplesItems;
querySchemaModel.columnsCollection.each(function (m) {
var columnName = m.get('name');
if (!_.contains(BLACKLISTED_COLUMNS, columnName)) {
var key = columnName + '-' + m.get('type');
var tuples = tuplesItems[key] = tuplesItems[key] || [];
tuples.push({
analysisDefinitionNodeModel: nodeDefModel,
layerDefinitionModel: layerDefinitionModel,
columnModel: m
});
}
});
return tuplesItems;
};
@@ -0,0 +1,104 @@
var $ = require('jquery');
var Backbone = require('backbone');
var createTuplesItems = require('./create-tuples-items');
var widgetsTypes = require('./widgets-types');
module.exports = Backbone.Model.extend({
defaults: {
render: false
},
initialize: function (attrs, opts) {
if (!opts.layerDefinitionModel) { throw new Error('layerDefinitionModel is required'); }
if (!opts.widgetDefinitionsCollection) { throw new Error('widgetDefinitionsCollection is required'); }
if (!opts.tableStats) { throw new Error('tablestats is required'); }
this._layerDefinitionModel = opts.layerDefinitionModel;
this._analysisDefinitionNode = this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
this._widgetDefinitionsCollection = opts.widgetDefinitionsCollection;
this._tableStats = opts.tableStats;
this._columnsCollection = new Backbone.Collection();
},
getCollection: function () {
return this._columnsCollection;
},
createColumnCollection: function () {
var tuplesItems = createTuplesItems(this._analysisDefinitionNode, this._layerDefinitionModel);
var models = widgetsTypes(tuplesItems);
this._columnsCollection.reset(models);
this._generateGraphs();
},
_generateGraphs: function () {
var self = this;
var promises = this._columnsCollection.map(function (stat) {
var table = stat.get('table');
var column = stat.get('name');
var deferred = new $.Deferred();
self._locateWidget(stat);
self._tableStats.graphFor(table, column, function (graph) {
if (graph.stats) {
stat.set({graph: graph});
}
deferred.resolve();
});
return deferred.promise();
}, this);
$.when.apply($, promises).done(function () {
self.set({render: self._columnsCollection.length > 0});
});
},
findWidget: function (model) {
var type = model.get('type');
var column = model.get('name');
return this._widgetDefinitionsCollection.findWhere({
type: type,
source: model.analysisDefinitionNodeModel().id,
column: column
});
},
_locateWidget: function (model) {
var widget = this.findWidget(model);
if (widget) {
model.set({
selected: true,
widget: widget
}, {silent: true});
}
},
getColumnsWithWidgetAndGraph: function () {
return this._columnsCollection.filter(function (model) {
return !!model.get('widget') && !!model.get('graph');
});
},
getColumnsWithWidget: function () {
return this._columnsCollection.filter(function (model) {
return !!model.get('widget') && !model.get('graph');
});
},
getColumnsWithGraph: function () {
return this._columnsCollection.filter(function (model) {
return !!model.get('graph') && !model.get('widget');
});
},
getColumnsWithoutGraph: function () {
return this._columnsCollection.filter(function (model) {
return !model.get('graph') && !model.get('widget');
});
}
});
@@ -0,0 +1,3 @@
<h2 class="CDB-Text CDB-Size-huge is-light u-secondaryTextColor">
<%= body %>
</h2>
@@ -0,0 +1,6 @@
<h2 class="CDB-Text CDB-Size-huge is-light u-secondaryTextColor">
<%= message %>
</h2>
<h2 class="CDB-Text CDB-Size-huge is-light u-secondaryTextColor u-tSpace-xl">
<%= action %>
</h2>
@@ -0,0 +1,53 @@
<% if (renderLoading) { %>
<div class="Data-loaderContainer">
<div class="CDB-Loader is-visible"></div>
</div>
<% } %>
<div class="FormPlaceholder-paragraph u-tSpace-m u-flex">
<div class="FormPlaceholder-step u-rSpace--m"></div>
<div class="FormPlaceholder-inner">
<span class="FormPlaceholder-label FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med"></span>
<span class="FormPlaceholder-label FormPlaceholder-size--short u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long"></span>
</div>
</div>
<div class="FormPlaceholder-paragraph u-tSpace-m u-flex">
<div class="FormPlaceholder-step u-rSpace--m"></div>
<div class="FormPlaceholder-inner">
<span class="FormPlaceholder-label FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med"></span>
<span class="FormPlaceholder-label FormPlaceholder-size--short u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long"></span>
</div>
</div>
<div class="FormPlaceholder-paragraph u-tSpace-m u-flex">
<div class="FormPlaceholder-step u-rSpace--m"></div>
<div class="FormPlaceholder-inner">
<span class="FormPlaceholder-label FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med"></span>
<span class="FormPlaceholder-label FormPlaceholder-size--short u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long"></span>
</div>
</div>
<div class="FormPlaceholder-paragraph u-tSpace-m u-flex">
<div class="FormPlaceholder-step u-rSpace--m"></div>
<div class="FormPlaceholder-inner">
<span class="FormPlaceholder-label FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--med"></span>
<span class="FormPlaceholder-label FormPlaceholder-size--short u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long u-tSpace-xl"></span>
<span class="FormPlaceholder-title FormPlaceholder-size--long"></span>
</div>
</div>
@@ -0,0 +1,271 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var StatView = require('./stat-view');
var dataNotReadyTemplate = require('./data-content-not-ready.tpl');
var dataErrorTemplate = require('./data-content-error.tpl');
var dataNoGeometryTemplate = require('./data-content-nogeometry.tpl');
var layerTabMessageTemplate = require('builder/editor/layers/layer-tab-message.tpl');
var actionErrorTemplate = require('builder/editor/layers/sql-error-action.tpl');
var addFeaturePointIcon = require('builder/components/icon/templates/add-feature-point.tpl');
var addFeatureLineIcon = require('builder/components/icon/templates/add-feature-line.tpl');
var addFeaturePolygonIcon = require('builder/components/icon/templates/add-feature-polygon.tpl');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var STATES = require('builder/data/query-base-status');
var REQUIRED_OPTS = [
'columnsModel',
'infoboxModel',
'overlayModel',
'queryGeometryModel',
'stackLayoutModel',
'userActions',
'layerContentModel',
'layerDefinitionModel'
];
module.exports = CoreView.extend({
module: 'editor/layers/layer-content-views/data/data-content-view',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initViewState();
this._initModels();
this._initBinds();
},
_initViewState: function () {
this._viewState = new Backbone.Model({
hasGeom: true,
isDataFiltered: false,
canBeGeoreferenced: false
});
this._setViewState();
},
_initModels: function () {
this._columnsCollection = this._columnsModel.getCollection();
this.model = new Backbone.Model({
renderLoading: true
});
},
_initBinds: function () {
this.listenTo(this._layerContentModel, 'change:state', this._setViewState);
this.listenTo(this._columnsModel, 'change:render', this._handleStats);
this.listenTo(this._columnsCollection, 'change:selected', this._handleWidget);
this.listenTo(this._overlayModel, 'change:visible', this._toggleOverlay);
this.listenTo(this._viewState, 'change', this.render);
},
render: function () {
this.clearSubViews();
this.$el.empty();
if (this._isErrored()) {
this._showError();
} else if (this._viewState.get('isDataFiltered')) {
this._showFilteredData();
} else if (this._viewState.get('canBeGeoreferenced')) {
this._showGeocodeMessage();
} else if (!this._viewState.get('hasGeom')) {
this._showNoGeometryData();
} else {
this._renderStats();
}
this._toggleOverlay();
return this;
},
_toggleOverlay: function () {
var isDisabled = this._overlayModel.get('visible');
this.$el.toggleClass('is-disabled', isDisabled);
},
_showLoading: function () {
this.$el.empty();
this.$el.append(
dataNotReadyTemplate({
renderLoading: this._layerContentModel.get('renderLoading')
})
);
},
_showError: function () {
this.$el.append(
dataErrorTemplate({
body: _t('editor.error-query.body', {
action: actionErrorTemplate({
label: _t('editor.error-query.label')
})
})
})
);
},
_showNoGeometryData: function () {
this.$el.append(
dataNoGeometryTemplate({
message: _t('editor.data.no-geometry-data.message'),
action: _t('editor.data.no-geometry-data.action-message', {
pointIcon: addFeaturePointIcon(),
lineIcon: addFeatureLineIcon(),
polygonIcon: addFeaturePolygonIcon()
})
})
);
},
_showFilteredData: function () {
this.$el.append(
layerTabMessageTemplate({
message: _t('editor.layers.warnings.no-data.message'),
action: _t('editor.layers.warnings.no-data.action-message')
})
);
},
_showGeocodeMessage: function () {
this.$el.append(
layerTabMessageTemplate({
message: _t('editor.layers.warnings.geocode.message'),
action: _t('editor.layers.warnings.geocode.action-message')
})
);
},
_isErrored: function () {
return this._layerContentModel.isErrored();
},
_columnsReady: function () {
return this._columnsModel.get('render') === true;
},
_handleWidget: function (model) {
model.get('selected') === true
? this._addWidget(model)
: this._destroyWidget(model);
},
_addWidget: function (model) {
var widgetModel = this._columnsModel.findWidget(model);
widgetModel
? model.set({widget: widgetModel})
: this._saveWidget(model);
},
_destroyWidget: function (model) {
var widgetModel = model.get('widget');
widgetModel && widgetModel.destroy();
},
_saveWidget: function (model) {
var self = this;
var widgetModel;
this._userActions.saveWidgetOption(model)
.then(function (newWidgetModel) {
widgetModel = newWidgetModel;
model.set({widget: widgetModel});
if (model.get('type') === 'time-series') {
self._normalizeTimeSeriesColumn(model);
}
return self._userActions.updateWidgetsOrder(model);
})
.then(function () {
self.render();
});
},
_normalizeTimeSeriesColumn: function (lastModel) {
var existingDefModel = this._columnsCollection.findWhere(function (model) {
return model.get('type') === 'time-series' && model.get('selected') === true && model.cid !== lastModel.cid;
});
existingDefModel && existingDefModel.set({
selected: false,
widget: null
});
},
_handleStats: function () {
if (this._columnsReady()) {
// we force render because the state could be ready already
this._layerContentModel.set({
state: STATES.fetched,
renderLoading: true
}, {
silent: true
});
this.render();
} else {
// if there are not stats, we hide loading and show placeholder
this._layerContentModel.set({
state: STATES.fetching,
renderLoading: false
});
}
},
_renderStats: function () {
if (!this._columnsReady()) {
return;
}
var withWidgetAndGraph = this._columnsModel.getColumnsWithWidgetAndGraph();
var withWidget = this._columnsModel.getColumnsWithWidget();
var withGraph = this._columnsModel.getColumnsWithGraph();
var withoutGraph = this._columnsModel.getColumnsWithoutGraph();
var all;
if (withWidgetAndGraph.length === 0 && withWidget.length === 0 &&
withGraph.length === 0 && withoutGraph.length === 0) {
this._infoboxModel.set({
state: 'no-data',
renderLoading: false
});
this._showLoading();
} else {
this.$el.empty();
all = _.union(withWidgetAndGraph, withWidget, withGraph, withoutGraph);
this._renderColumns(all);
}
},
_renderColumns: function (stats) {
_.each(stats, function (stat, index) {
var view = new StatView({
stackLayoutModel: this._stackLayoutModel,
statModel: stat
});
this.addView(view);
this.$el.append(view.render().el);
}, this);
},
_setViewState: function () {
var self = this;
var hasGeomPromise = this._queryGeometryModel.hasValueAsync();
var isDataFilteredPromise = this._layerDefinitionModel.isDataFiltered();
var geoReferencePromise = this._layerDefinitionModel.canBeGeoreferenced();
Promise.all([hasGeomPromise, isDataFilteredPromise, geoReferencePromise])
.then(function (values) {
self._viewState.set({
hasGeom: values[0],
isDataFiltered: values[1],
canBeGeoreferenced: values[2]
});
});
}
});
@@ -0,0 +1,43 @@
var Backbone = require('backbone');
var _ = require('underscore');
var UndoManager = require('builder/data/undo-manager.js');
/**
* Data SQL Undo-redo model
*/
module.exports = Backbone.Model.extend({
defaults: {
content: ''
},
initialize: function (attrs, opts) {
this._history = this._generateHistory(opts && opts.history);
UndoManager.init(this, {
track: true,
history: this._history
});
},
_generateHistory: function (history) {
if (history && history.length) {
var data = _.reduce(history, function (memo, sql) {
memo.push({
content: sql
});
return memo;
}, [], this);
return data;
}
return false;
},
getHistory: function () {
return _.pluck(
this.getUndoHistory(),
'content'
);
}
});
@@ -0,0 +1,74 @@
var CoreView = require('backbone/core-view');
var CodeMirrorView = require('builder/components/code-mirror/code-mirror-view');
var ErrorTemplate = require('./code-mirror-error.tpl');
var FactoryHints = require('builder/editor/editor-hints/factory-hints');
var SQLHints = require('builder/editor/editor-hints/sql-hints');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'layerDefinitionModel',
'querySchemaModel',
'codemirrorModel',
'onApplyEvent'
];
module.exports = CoreView.extend({
module: 'editor:layers:layer-content-views:data-sql-view',
className: 'Editor-dataContentSQL Editor-content',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
FactoryHints.init({
querySchemaModel: this._querySchemaModel,
layerDefinitionModel: this._layerDefinitionModel,
tokens: SQLHints
});
},
render: function () {
this.clearSubViews();
this.$el.empty();
this._initViews();
return this;
},
_initBinds: function () {
this._querySchemaModel.on('change:query', this._onQueryChanged, this);
this.add_related_model(this._querySchemaModel);
},
_onQueryChanged: function (mdl, status) {
if (status === 'fetched') {
this.codeMirrorView.updateHints(SQLHints.reset().hints);
}
},
_initViews: function () {
var hints = FactoryHints.reset().hints;
this.codeMirrorView = new CodeMirrorView({
model: this._codemirrorModel,
hints: hints,
mode: 'text/x-pgsql',
tips: [
_t('editor.data.code-mirror.tip')
],
errorTemplate: ErrorTemplate
});
this.codeMirrorView.bind('codeSaved', this._triggerCodeSaved, this);
this.addView(this.codeMirrorView);
this.$el.append(this.codeMirrorView.render().el);
},
_updateEditorContent: function () {
this.codeMirrorView.setContent(this._layerDefinitionModel.get('sql'));
},
_triggerCodeSaved: function (code, view) {
this._onApplyEvent && this._onApplyEvent();
}
});
@@ -0,0 +1,466 @@
var _ = require('underscore');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var PanelWithOptionsView = require('builder/components/view-options/panel-with-options-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var DatasetBaseView = require('builder/components/dataset/dataset-base-view');
var DataContentView = require('./data-content-view');
var DataSQLView = require('./data-sql-view');
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
var Toggler = require('builder/components/toggler/toggler-view');
var UndoButtons = require('builder/components/undo-redo/undo-redo-view');
var Infobox = require('builder/components/infobox/infobox-factory');
var InfoboxModel = require('builder/components/infobox/infobox-model');
var InfoboxCollection = require('builder/components/infobox/infobox-collection');
var TableStats = require('builder/components/modals/add-widgets/tablestats');
var DataColumnsModel = require('./data-columns-model');
var SQLNotifications = require('builder/sql-notifications');
var MetricsTracker = require('builder/components/metrics/metrics-tracker');
var MetricsTypes = require('builder/components/metrics/metrics-types');
var OnboardingLauncher = require('builder/components/onboardings/generic/generic-onboarding-launcher');
var OnboardingView = require('builder/components/onboardings/layers/data-onboarding/data-onboarding-view');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var LocaleHelper = require('builder/helpers/locale');
var ONBOARDING_KEY = 'layer-data-onboarding';
var REQUIRED_OPTS = [
'widgetDefinitionsCollection',
'stackLayoutModel',
'userActions',
'layerDefinitionModel',
'userModel',
'onboardings',
'onboardingNotification',
'layerContentModel'
];
var getNotificationMessage = function (error, type) {
var prefix = 'notifications.sql.';
var key = prefix + type;
var result = LocaleHelper.linkify(key);
return result || (error && _.first(error)) || _t('notifications.sql.unknown.body');
};
module.exports = DatasetBaseView.extend({
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._nodeModel = opts.layerDefinitionModel.getAnalysisDefinitionNodeModel();
this._querySchemaModel = this._nodeModel.querySchemaModel;
this._queryGeometryModel = this._nodeModel.queryGeometryModel;
DatasetBaseView.prototype.initialize.call(this, {
layerDefinitionModel: opts.layerDefinitionModel,
editorModel: opts.editorModel,
configModel: opts.configModel,
querySchemaModel: this._querySchemaModel
});
this._initModels();
this._tableStats = new TableStats({
configModel: this._configModel,
userModel: this._userModel
});
this._columnsModel = new DataColumnsModel({}, {
layerDefinitionModel: this._layerDefinitionModel,
widgetDefinitionsCollection: this._widgetDefinitionsCollection,
tableStats: this._tableStats
});
SQLNotifications.track(this);
this._parseSQL = this._internalParseSQL.bind(this, this._afterAlterDataSuccess);
this._onboardingLauncher = new OnboardingLauncher({
view: OnboardingView,
onboardingNotification: this._onboardingNotification,
notificationKey: ONBOARDING_KEY,
onboardings: this._onboardings
}, {
editorModel: this._editorModel,
selector: 'LayerOnboarding'
});
this._configPanes();
this._initBinds();
this._initData();
},
render: function () {
this._launchOnboarding();
this.clearSubViews();
this.$el.empty();
this._initViews();
return this;
},
_initModels: function () {
this._infoboxModel = new InfoboxModel({
state: this._isLayerHidden()
? 'layer-hidden'
: ''
});
this._layerDefinitionModel.canBeGeoreferenced()
.then(function (canBeGeoreferenced) {
if (!this._isLayerHidden() && canBeGeoreferenced) {
this._infoboxModel.set('state', 'georeference');
}
}.bind(this));
this._overlayModel = new Backbone.Model({
visible: this._isLayerHidden()
});
this._applyButtonStatusModel = new Backbone.Model({
loading: false
});
this._togglerModel = new Backbone.Model({
labels: [_t('editor.data.data-toggle.values'), _t('editor.data.data-toggle.cartocss')],
active: this._editorModel.isEditing(),
disabled: this._editorModel.isDisabled(),
tooltip: _t('editor.data.data-toggle.tooltip')
});
},
_launchOnboarding: function () {
if (this._onboardingNotification.getKey(ONBOARDING_KEY)) {
return;
}
if (!this._editorModel.isEditing()) {
this._onboardingLauncher.launch({
numberOfWidgets: this._widgetDefinitionsCollection.length,
hasTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries(),
hasAnimatedTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries({ animated: true })
});
}
},
_initData: function () {
if (this._hasFetchedQuerySchema()) {
this._onQuerySchemaStatusChange();
}
},
_redirectIfErrors: function () {
var errors = this._querySchemaModel.get('query_errors');
if (errors && errors.length > 0) {
this._showErrors();
this._editorModel.set({
edition: true
});
}
},
_onQuerySchemaChange: function () {
if (this._hasFetchedQuerySchema()) {
this._codemirrorModel.set({ content: this._querySchemaModel.get('query') });
}
},
_onQuerySchemaStatusChange: function () {
if (this._hasFetchedQuerySchema()) {
this._columnsModel.createColumnCollection();
}
},
_hasFetchedQuerySchema: function () {
return this._querySchemaModel.isFetched();
},
_initBinds: function () {
this.listenTo(this._editorModel, 'change:edition', this._onChangeEdition);
this.listenTo(this._editorModel, 'change:disabled', this._onChangeDisabled);
this.listenTo(this._togglerModel, 'change:active', this._onTogglerChanged);
this.listenTo(this._querySchemaModel, 'change:query', this._onQuerySchemaChange);
this.listenTo(this._querySchemaModel, 'change:status', this._onQuerySchemaStatusChange);
this.listenTo(this._querySchemaModel, 'change:query_errors', this._showErrors);
this.listenTo(this._sqlModel, 'undo redo', function () {
this._codemirrorModel.set('content', this._sqlModel.get('content'));
}.bind(this));
this.listenTo(this._layerDefinitionModel, 'change:visible', this._infoboxState);
this._querySchemaModel.bind('change:status', function () {
this._applyButtonStatusModel.set('loading', this._querySchemaModel.isFetching());
}, this);
},
_runQuery: function (query, callback) {
// Should change the query to geometry model before the schema model
this._queryGeometryModel.set({
query: query
});
this._querySchemaModel.set({
query: query,
status: 'unfetched'
});
SQLNotifications.showNotification({
status: 'loading',
info: _t('notifications.sql.applying'),
closable: false
});
var saveSQL = _.after(2, callback);
var errorSQL = function (err) {
var type = err && err.status;
var error = getNotificationMessage(err.error, type);
this._forceErrors(error, {
showEditorError: type === 400
});
}.bind(this);
this._querySchemaModel.fetch({ success: saveSQL, error: errorSQL });
this._queryGeometryModel.fetch({ complete: saveSQL, error: errorSQL });
},
_saveSQL: function (response) {
if (response.status !== 200) {
return;
}
var query = this._codemirrorModel.get('content');
this._sqlModel.set('content', query);
this._userActions.saveAnalysisSourceQuery(query, this._nodeModel, this._layerDefinitionModel);
this._querySchemaModel.set('query_errors', []);
SQLNotifications.showNotification({
status: 'success',
info: _t('notifications.sql.success'),
closable: true
});
this._checkClearButton();
MetricsTracker.track(MetricsTypes.APPLIED_SQL, {
node_id: this._nodeModel.get('id'),
sql: this._nodeModel.get('query')
});
MetricsTracker.track(MetricsTypes.USED_ADVANCED_MODE, {
mode_type: 'sql'
});
},
_isQueryAlreadyApplied: function (query) {
var history = this._sqlModel.getUndoHistory();
var last = _.last(history);
return last && last.content && last.content.toLowerCase() === query.toLowerCase();
},
_updateVisNotification: function () {
SQLNotifications.showNotification({
status: 'success',
info: _t('notifications.sql.success'),
closable: true
});
},
_defaultSQL: function () {
return this._nodeModel.getDefaultQuery();
},
_afterAlterDataSuccess: function () {
var originalQuery = this._defaultSQL();
this._userActions.saveAnalysisSourceQuery(originalQuery, this._nodeModel, this._layerDefinitionModel);
},
_hasAnalysisApplied: function () {
return this._nodeModel.get('type') !== 'source';
},
_onChangeEdition: function () {
this._infoboxState();
var edition = this._editorModel.get('edition');
var index = edition ? 1 : 0;
this._collectionPane.at(index).set({ selected: true });
this._togglerModel.set({ active: edition });
},
_onChangeDisabled: function () {
var disabled = this._editorModel.get('disabled');
this._togglerModel.set({ disabled: disabled });
},
_onTogglerChanged: function () {
var checked = this._togglerModel.get('active');
this._editorModel.set({ edition: checked });
},
_configPanes: function () {
var self = this;
var tabPaneTabs = [{
selected: !this._editorModel.get('edition'),
createContentView: function () {
return new ScrollView({
createContentView: function () {
return new DataContentView({
className: 'Editor-content',
stackLayoutModel: self._stackLayoutModel,
columnsModel: self._columnsModel,
queryGeometryModel: self._queryGeometryModel,
userActions: self._userActions,
infoboxModel: self._infoboxModel,
overlayModel: self._overlayModel,
layerContentModel: self._layerContentModel,
layerDefinitionModel: self._layerDefinitionModel
});
}
});
},
createActionView: function () {
return new CoreView();
}
}, {
selected: this._editorModel.get('edition'),
createContentView: function () {
return new DataSQLView({
layerDefinitionModel: self._layerDefinitionModel,
querySchemaModel: self._querySchemaModel,
codemirrorModel: self._codemirrorModel,
onApplyEvent: self._parseSQL
});
},
createActionView: function () {
var isAnalysis = self._hasAnalysisApplied();
if (!isAnalysis) {
self._checkClearButton();
return new UndoButtons({
trackModel: self._sqlModel,
editorModel: self._editorModel,
clearModel: self._clearSQLModel,
applyStatusModel: self._applyButtonStatusModel,
applyButton: true,
clearButton: true,
onApplyClick: self._parseSQL,
onClearClick: self._clearSQL.bind(self)
});
}
}
}];
this._collectionPane = new TabPaneCollection(tabPaneTabs);
},
_confirmReadOnly: function () {
this._infoboxModel.set({ state: '' });
},
_initViews: function () {
var self = this;
var infoboxSstates = [
{
state: 'readonly',
createContentView: function () {
return Infobox.createWithAction({
type: 'code',
title: _t('editor.data.messages.sql-readonly.title'),
body: _t('editor.data.messages.sql-readonly.body'),
action: {
label: _t('editor.data.messages.sql-readonly.accept')
}
});
},
onAction: self._confirmReadOnly.bind(self)
}, {
state: 'no-data',
createContentView: function () {
return Infobox.createInfo({
type: 'alert',
title: _t('editor.data.messages.empty-data.title'),
body: _t('editor.data.messages.empty-data.body')
});
}
}, {
state: 'layer-hidden',
createContentView: function () {
return Infobox.createWithAction({
type: 'alert',
title: _t('editor.messages.layer-hidden.title'),
body: _t('editor.messages.layer-hidden.body'),
action: {
label: _t('editor.messages.layer-hidden.show')
}
});
},
onAction: self._showHiddenLayer.bind(self)
}
];
var infoboxCollection = new InfoboxCollection(infoboxSstates);
var panelWithOptionsView = new PanelWithOptionsView({
className: 'Editor-content',
editorModel: self._editorModel,
infoboxModel: self._infoboxModel,
infoboxCollection: infoboxCollection,
createContentView: function () {
return new TabPaneView({
collection: self._collectionPane
});
},
createControlView: function () {
return new Toggler({
model: self._togglerModel
});
},
createActionView: function () {
return new TabPaneView({
collection: self._collectionPane,
createContentKey: 'createActionView'
});
}
});
this.$el.append(panelWithOptionsView.render().el);
this.addView(panelWithOptionsView);
// TOFIX
// If there are errors in the SQL, open the sql editor by default
// we defer it to not interfere with the high order tabpane item selection
setTimeout(this._redirectIfErrors.bind(this), 0);
},
_infoboxState: function () {
var edition = this._editorModel.get('edition');
var isAnalysis = this._hasAnalysisApplied();
if (edition && isAnalysis) {
this._codemirrorModel.set({ readonly: true });
this._infoboxModel.set({ state: 'readonly' });
} else if (this._isLayerHidden()) {
this._infoboxModel.set({ state: 'layer-hidden' });
this._overlayModel.set({ visible: true });
} else {
this._codemirrorModel.set({ readonly: false });
this._infoboxModel.set({ state: '' });
this._overlayModel.set({ visible: false });
}
},
_showHiddenLayer: function () {
var savingOptions = {
shouldPreserveAutoStyle: true
};
this._layerDefinitionModel.toggleVisible();
this._userActions.saveLayer(this._layerDefinitionModel, savingOptions);
},
_isLayerHidden: function () {
return this._layerDefinitionModel.get('visible') === false;
}
});
@@ -0,0 +1,5 @@
<ul class="u-flex CDB-Text CDB-Size-small u-upperCase u-bSpace u-secondaryTextColor">
<li class='u-rSpace--m js-null'></li>
<li class='js-percent'></li>
</ul>
<div class="js-category-stat"></div>
@@ -0,0 +1 @@
<h4 class="CDB-Text CDB-Size-huge js-formula-stat"></h4>
@@ -0,0 +1,2 @@
<div class="CDB-Text CDB-Size-small u-upperCase u-bSpace u-secondaryTextColor js-histogram-numbers"></div>
<div class="js-histogram-stat"></div>
@@ -0,0 +1,137 @@
var _ = require('underscore');
var CoreView = require('backbone/core-view');
var template = require('./stat.tpl');
var utils = require('builder/helpers/utils');
var templateCategory = require('./stat-category.tpl');
var templateHistogram = require('./stat-histogram.tpl');
var templateFormula = require('./stat-formula.tpl');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var Router = require('builder/routes/router');
module.exports = CoreView.extend({
module: 'editor/layers/layer-content-views/data/stat-view',
className: 'StatsList',
events: {
'click .js-checkbox': '_onSelect',
'click .js-style': '_onEdit'
},
initialize: function (opts) {
if (!opts.statModel) throw new Error('statModel is required');
this._statModel = opts.statModel;
this._initBinds();
},
render: function () {
this.clearSubViews();
this.$el.empty();
this._initViews();
return this;
},
_initBinds: function () {
this._statModel.on('change:selected', this.render, this);
this.add_related_model(this._statModel);
},
_initViews: function () {
var graph = this._statModel.get('graph');
var type = this._statModel.get('type');
this._renderTemplate();
if (graph) {
// No graph for formula
if (type === 'histogram') {
this._showHistogram(graph);
} else if (type === 'category') {
this._showCategory(graph);
} else if (type === 'formula') {
this._showFormula(graph);
}
}
if (this._isSelected()) {
var tooltip = new TipsyTooltipView({
el: this.$('.js-help'),
gravity: 'w',
title: function () {
return _t('editor.data.stats.help');
}
});
this.addView(tooltip);
}
},
_renderTemplate: function () {
this.$el.append(template({
id: this.cid,
column: this._statModel.get('title'),
type: this._statModel.get('column'),
graph: this._statModel.get('type'),
isSelected: this._isSelected()
}));
},
_isSelected: function () {
return this._statModel.get('selected');
},
_onSelect: function () {
this._statModel.set('selected', !this._statModel.get('selected'));
},
_onEdit: function (event) {
event.preventDefault();
Router.goToWidget(this._statModel.get('widget').get('id'));
},
_showCategory: function (graph) {
if (graph.stats && graph.stats.freqs) {
this.$('.js-stat').append(templateCategory());
this.$('.js-category-stat').append(graph.getCategory({
color: '#9DE0AD',
width: 262,
height: 10
}));
this.$('.js-null').text(utils.formatDecimalPositions(graph.getNullsPercentage() * 100) + _t('editor.data.stats.null'));
if (this.options.statModel.attributes.column !== 'boolean') {
this.$('.js-percent').text(utils.formatDecimalPositions(graph.getPercentageInTopCategories() * 100) + _t('editor.data.stats.top-cat'));
} else {
var numOfTrues = graph.getTrues();
if (utils.isNumeric(numOfTrues)) {
this.$('.js-percent').text(utils.formatDecimalPositions(numOfTrues * 100) + _t('editor.data.stats.trues'));
}
}
}
},
_showHistogram: function (graph) {
if (graph.stats) {
this.$('.js-stat').append(templateHistogram());
this.$('.js-histogram-stat').append(graph.getHistogram({
color: '#9DE0AD',
width: 262,
height: 20,
bins: 20
}));
this.$('.js-histogram-numbers').text(graph.getNullsPercentage() + _t('editor.data.stats.null'));
}
},
_showFormula: function (graph) {
var aggregation;
if (graph.stats) {
aggregation = graph.getCount();
if (_.isNumber(aggregation)) {
this.$('.js-stat').append(templateFormula());
this.$('.js-formula-stat').text(aggregation.toFixed().toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','));
}
}
}
});
@@ -0,0 +1,22 @@
<div class="StatsList-item">
<div class="u-rSpace--m">
<input class="CDB-Checkbox js-checkbox" id="<%- id %>" type="checkbox" <% if (isSelected) { %>checked="checked"<% } %> />
<span class="u-iBlock CDB-Checkbox-face"></span>
</div>
<div class="WidgetList-inner js-inner">
<div class="StatsList-header u-flex u-justifySpace u-alignCenter u-bSpace--m">
<label class="u-ellipsis CDB-Text CDB-Size-medium" for="<%- id %>"><%- _t('editor.data.stats.add-widget') %></label>
<% if (isSelected) { %>
<button class="StatsList-style CDB-Text CDB-Size-small js-style u-actionTextColor js-help">
<div class="StatsList-arrow CDB-Shape-Arrow is-blue u-iBlock u-rSpace--m"></div> <%- _t('editor.data.stats.edit') %>
</button>
<% } %>
</div>
<div class="u-flex u-alignCenter u-bSpace">
<h2 class="js-title u-ellipsis CDB-Text CDB-Size-large u-rSpace--m"><%- column %></h2>
<div class="StatsList-tag Tag Tag--outline Tag-outline--grey CDB-Text CDB-Size-small u-upperCase"><%- type %></div>
</div>
<div class="js-stat"></div>
</div>
</div>
@@ -0,0 +1,60 @@
var _ = require('underscore');
var FormulaOptionModel = require('builder/components/modals/add-widgets/formula/formula-option-model');
var CategoryOptionModel = require('builder/components/modals/add-widgets/category/category-option-model');
var HistogramOptionModel = require('builder/components/modals/add-widgets/histogram/histogram-option-model');
var TimeSeriesOptionModel = require('builder/components/modals/add-widgets/time-series/time-series-option-model');
var CARTODB_ID = 'cartodb_id';
module.exports = function (tuplesItems) {
return _.reduce(tuplesItems, function (memo, tuples) {
var columnModel = tuples[0].columnModel;
var layerDef = tuples[0].layerDefinitionModel;
var table = layerDef.getTableName();
var type = columnModel.get('type');
var columnName = columnModel.get('name');
var model;
if (columnName === CARTODB_ID) {
model = new FormulaOptionModel({
tuples: tuples,
table: table,
title: _t('editor.data.stats.feature-count'),
column: columnModel.get('type'),
operation: 'count',
name: columnName
});
memo.push(model);
} else if (type === 'string' || type === 'boolean') {
model = new CategoryOptionModel({
tuples: tuples,
table: table,
title: columnName,
name: columnName,
column: columnModel.get('type'),
aggregation: 'count' // or sum
});
memo.push(model);
} else if (columnModel.get('type') === 'number') {
model = new HistogramOptionModel({
tuples: tuples,
table: table,
title: columnName,
name: columnName,
column: columnModel.get('type'),
bins: 10
});
memo.push(model);
} else if (columnModel.get('type') === 'date') {
model = new TimeSeriesOptionModel({
tuples: tuples,
table: table,
title: columnName,
name: columnName,
column: columnModel.get('type')
});
memo.push(model);
}
return memo;
}, []);
};