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,117 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const PagesSubheader = require('dashboard/components/pages-subheader/pages-subheader.js');
const AccountFormView = require('./account-form-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./account-content.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'configModel',
'flashMessageModel',
'client'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
},
_initBinds: function () {
this.listenTo(this.model, 'change:isLoading change:errors', this.render);
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
return this;
},
_initModels: function () {
this.model = new Backbone.Model();
this.modals = new ModalsServiceModel();
},
_initViews: function () {
const pagesSubheader = new PagesSubheader({
userModel: this._userModel,
configModel: this._configModel
});
this.$('.js-SideMenu').append(pagesSubheader.render().el);
this.addView(pagesSubheader);
const accountFormView = new AccountFormView({
userModel: this._userModel,
renderModel: this.model,
configModel: this._configModel,
modals: this.modals,
setLoading: this._setLoading.bind(this),
onSuccess: this._showSuccess.bind(this),
onError: this._showErrors.bind(this),
client: this._client,
errors: this.model.get('errors')
});
this.$('.js-AccountContent').append(accountFormView.render().el);
this.addView(accountFormView);
},
_setLoading: function (message) {
this._flashMessageModel.hide();
this.model.set({
isLoading: !!message,
loadingText: message,
errors: []
});
},
_setFlashMessage: function (data, message, type) {
this._setLoading('');
const jsonData = data && data.responseJSON || {};
const errors = jsonData.errors;
let flashMessage = jsonData.message;
if (errors) {
this.model.set({ errors });
}
if (!flashMessage) {
flashMessage = message;
}
this._flashMessageModel.show(flashMessage, type);
},
_showSuccess: function (data) {
$(window).scrollTop(0);
_.extend(
this._userModel.attributes,
data.user_data
);
this._setFlashMessage(data, _t('account.flash_messages.save_changes.success'), 'success');
if (data.mfa_required) {
this._goToMultifactorAuthentication();
}
},
_goToMultifactorAuthentication: function () {
window.location = '/multifactor_authentication';
},
_showErrors: function (data) {
$(window).scrollTop(0);
this._setFlashMessage(data, _t('account.flash_messages.save_changes.error'), 'error');
}
});
@@ -0,0 +1,13 @@
<div class="CDB-Text FormAccount-Section u-inner">
<div class="SideMenu CDB-Text CDB-Size-medium js-SideMenu"></div>
<div class="FormAccount-Content">
<div class="FormAccount-title">
<p class="FormAccount-titleText"><%= _t('account.views.content.form_title') %></p>
</div>
<span class="FormAccount-separator"></span>
<div class="js-AccountContent"></div>
</div>
</div>
@@ -0,0 +1,211 @@
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const randomQuote = require('builder/components/loading/random-quote');
const ServiceItem = require('dashboard/components/service-item/service-item-view');
const template = require('./account-form.tpl');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const DeleteAccountView = require('dashboard/components/delete-account/delete-account-view');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'renderModel',
'configModel',
'modals',
'setLoading',
'onSuccess',
'onError',
'client'
];
module.exports = CoreView.extend({
events: {
'click .js-save': '_onClickSave',
'submit form': '_onClickSave',
'change .js-toggle-mfa': '_onToggleMfa'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
},
_initModels: function () {
this._errors = this.options.errors || {};
this.add_related_model(this._renderModel);
},
_initBinds: function () {
this._renderModel.bind('change:isLoading', this.render, this);
},
render: function () {
this.clearSubViews();
if (this._renderModel.get('isLoading')) {
this.$el.html(
loadingTemplate({
title: this._renderModel.get('loadingText'),
descHTML: randomQuote()
})
);
} else {
this.$el.html(template({
isCartoDBHosted: this._configModel.get('cartodb_com_hosted'),
formAction: this._configModel.prefixUrl() + '/account',
username: this._getField('username'),
errors: this._errors,
isInsideOrg: this._userModel.isInsideOrg(),
isAuthUsernamePasswordEnabled: this._getField('auth_username_password_enabled'),
hidePasswordFields: this._userModel.isInsideOrg() && !this._getField('auth_username_password_enabled'),
canChangePassword: this._getField('can_change_password'),
isOrgOwner: this._userModel.isOrgOwner(),
planName: this._getField('plan_name'),
planUrl: this._getField('plan_url'),
cantBeDeletedReason: this._getField('cant_be_deleted_reason'),
services: this._getField('services') || [],
mfaEnabled: this._getField('mfa_configured')
}));
this._initViews();
}
return this;
},
_setDeleteAccountView: function (event) {
this._modals.create(modalModel =>
new DeleteAccountView({
userModel: this._userModel,
modalModel,
onError: this._onError,
client: this._client
})
);
},
_initViews: function () {
const services = this._getField('services');
this.$('.js-deleteAccount').click(event => {
event && event.preventDefault();
this._setDeleteAccountView(event);
});
if (services && services.length) {
_.each(services, function (service) {
const serviceItem = new ServiceItem({
model: new Backbone.Model(_.extend({ state: 'idle' }, service)),
configModel: this._configModel
});
this.$('.js-datasourcesContent').after(serviceItem.render().el);
this.addView(serviceItem);
}, this);
}
},
_getField: function (field) {
return this._userModel.get(field);
},
_onClickSave: function (event) {
this.killEvent(event);
const origin = this._getUserFields();
const destination = this._getDestinationValues();
const destinationKeys = _.keys(destination);
const differenceKeys = _.filter(destinationKeys, key =>
origin[key] !== destination[key]
);
const user = _.pick(destination, differenceKeys);
if (!this._userModel.get('needs_password_confirmation')) {
return this._updateUser(user);
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: (password) => this._updateUser(user, password),
updatePassword: destination.new_password !== '' && destination.confirm_password !== ''
});
},
_onToggleMfa: function (event) {
this.killEvent(event);
const newLabel = this._mfaStatus() ? _t('account.views.form.mfa_enabled') : _t('account.views.form.mfa_disabled');
this._mfaLabel().html(newLabel);
},
_updateUser: function (user, password) {
this._setLoading('Saving changes');
const userParams = { user: { ...user, password_confirmation: password } };
this._client.putConfig(userParams, (errors, response, data) => {
if (errors) {
this.options.onError(data, errors);
this.render();
} else {
this._getUser();
}
});
},
_getUser: function () {
this._client.getConfig((errors, response, data) => {
if (errors) {
this.options.onError(data, response, errors);
} else {
this.options.onSuccess(data);
}
this.render();
});
},
_getUserFields: function () {
return {
username: this._getField('username')
};
},
_getDestinationValues: function () {
return {
username: this._username(),
new_password: this._newPassword(),
confirm_password: this._confirmPassword(),
mfa: this._mfaStatus()
};
},
_username: function () {
return this.$('#user_username').val();
},
_newPassword: function () {
return this.$('#user_new_password').val();
},
_confirmPassword: function () {
return this.$('#confirm_password').val();
},
_mfaStatus: function () {
if (this.$('.js-toggle-mfa').length === 0) {
return false;
}
return this.$('.js-toggle-mfa')[0].checked;
},
_mfaLabel: function () {
return this.$('.js-mfa-label');
}
});
@@ -0,0 +1,127 @@
<form accept-charset="UTF-8">
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.username') %></label>
</div>
<div class="FormAccount-rowData">
<input class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med is-disabled" id="user_username" name="user[username]" readonly="readonly" size="30" type="text" value="<%= username %>">
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
<p class="CDB-Text CDB-Size-small u-altTextColor"><%= _t('account.views.form.subdomain_info') %></p>
</div>
</div>
</div>
<% if (!hidePasswordFields) { %>
<div class="VerticalAligned--FormRow">
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.new_password') %></label>
</div>
<div class="FormAccount-rowData">
<input class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med <% if (errors['new_password']) { %>has-error<% } %> <% if (!canChangePassword) { %>is-disabled<% } %>" id="user_new_password" name="user[new_password]" size="30" type="password" <% if (!canChangePassword) { %>readonly="readonly"<% } %>>
</div>
<div class="FormAccount-rowInfo">
<% if (errors['new_password']) { %>
<p class="FormAccount-rowInfoText FormAccount-rowInfoText--error u-tSpace"><%= errors['new_password'][0] %></p>
<% } %>
</div>
</div>
<div class="FormAccount-row js-confirmPassword">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.confirm_password') %></label>
</div>
<div class="FormAccount-rowData">
<input class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med <% if (!canChangePassword) { %>is-disabled<% } %>" id="confirm_password" name="user[confirm_password]" size="30" type="password" <% if (!canChangePassword) { %>readonly="readonly"<% } %>>
</div>
</div>
</div>
<% } %>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor">
<%= _t('account.views.form.multifactor_authentication') %>
</label>
</div>
<div class="FormAccount-rowData u-tspace-s u-vspace-s">
<div class="Toggler">
<input name="user[mfa]" type="hidden" value="0">
<input class="js-toggle-mfa" id="mfa" name="user[mfa]" type="checkbox" value="1" <% if (mfaEnabled) { %>checked="checked"<% } %>>
<label for="mfa"></label>
</div>
<div class="FormAccount-rowInfo u-lSpace--xl">
<p class="CDB-Text CDB-Size-medium js-mfa-label">
<%= mfaEnabled ? _t('account.views.form.mfa_enabled') : _t('account.views.form.mfa_disabled') %>
</p>
</div>
</div>
<div class="FormAccount-rowData u-tspace-xs">
<p class="CDB-Text CDB-Size-small u-altTextColor"><%= _t('account.views.form.mfa_description') %></p>
</div>
</div>
<% if ((!isInsideOrg || isOrgOwner) && !isCartoDBHosted) { %>
<div class="FormAccount-title">
<p class="FormAccount-titleText"><%= _t('account.views.form.account_type') %></p>
</div>
<span class="FormAccount-separator"></span>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.billing_plan') %></label>
</div>
<div class="FormAccount-rowData">
<div class="FormAccount-planTag CDB-Size-medium"><%= planName %></div>
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
<p class="FormAccount-rowInfoText CDB-Size-medium"><a href="<%= planUrl %>" class="FormAccount-link"><%= _t('account.views.form.view_details') %></a></p>
</div>
</div>
</div>
<% } %>
<% if (services.length > 0) { %>
<div class="FormAccount-title">
<p class="FormAccount-titleText"><%= _t('account.views.form.connect_external_datasources') %></p>
</div>
<span class="FormAccount-separator"></span>
<div class="js-datasourcesContent"></div>
<% } %>
<div class="FormAccount-footer <% if (cantBeDeletedReason) { %>FormAccount-footer--noMarginBottom<% } %>">
<% if (cantBeDeletedReason) { %>
<p class="FormAccount-footerText">
<i class="CDB-IconFont CDB-IconFont-info FormAccount-footerIcon"></i>
<span><%= cantBeDeletedReason %></span>
</p>
<% } else { %>
<p class="FormAccount-footerText"></p>
<% } %>
<button type="submit" class="CDB-Button CDB-Button--primary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"><%= _t('account.views.form.save_changes') %></span>
</button>
</div>
<% if (!cantBeDeletedReason) { %>
<div class="FormAccount-title">
<p class="FormAccount-titleText"><%= _t('account.views.form.delete_account') %></p>
</div>
<span class="FormAccount-separator"></span>
<div class="FormAccount-row FormAccount-row--wideMarginBottom">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= _t('account.views.form.confirm') %></label>
</div>
<div class="FormAccount-rowData">
<span class="FormAccount-button--deleteAccount CDB-Size-medium js-deleteAccount"><%= _t('account.views.form.delete_all') %></span>
</div>
</div>
<% } %>
</form>
@@ -0,0 +1,77 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const TrialNotificationView = require('dashboard/components/trial-notification/trial-notification-view');
const AccountContentView = require('./account-content-view');
const UpgradeMessage = require('dashboard/components/upgrade-message-view.js');
const VendorScriptsView = require('dashboard/components/vendor-scripts/vendor-scripts-view');
const FlashMessageModel = require('dashboard/data/flash-message-model');
const FlashMessageView = require('dashboard/components/flash-message/flash-message-view');
var moment = require('moment');
const TRIAL_ACCOUNTS = ['PERSONAL30', 'Individual'];
const REQUIRED_OPTS = [
'userModel',
'configModel',
'client',
'assetsVersion',
'organizationNotifications'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initViews();
},
_initViews: function () {
const $app = this.$('#app');
const accountType = this._userModel.get('account_type');
if (TRIAL_ACCOUNTS.indexOf(accountType) > -1) {
const trialEndsAt = moment(this._userModel.get('trial_ends_at'));
const now = moment();
const trialNotificationView = new TrialNotificationView({
userModel: this._userModel,
upgradeUrl: this._configModel.get('upgrade_url'),
trialDays: Math.round(trialEndsAt.diff(now, 'days', true))
});
$app.append(trialNotificationView.render().el);
this.addView(trialNotificationView);
}
const flashMessageModel = new FlashMessageModel();
const flashMessageView = new FlashMessageView({
model: flashMessageModel
});
$app.prepend(flashMessageView.render().el);
this.addView(flashMessageView);
const accountContentView = new AccountContentView({
userModel: this._userModel,
configModel: this._configModel,
flashMessageModel,
client: this._client
});
$app.append(accountContentView.render().el);
this.addView(accountContentView);
const upgradeMessage = new UpgradeMessage({
userModel: this._userModel,
configModel: this._configModel
});
$app.prepend(upgradeMessage.render().el);
this.addView(upgradeMessage);
const vendorScriptsView = new VendorScriptsView({
configModel: this._configModel,
assetsVersion: this._assetsVersion,
userModel: this._userModel
});
this.$el.append(vendorScriptsView.render().el);
this.addView(vendorScriptsView);
return this;
}
});
@@ -0,0 +1,17 @@
const Backbone = require('backbone');
/**
* Header view model to handle state for dashboard header view.
*/
module.exports = Backbone.Model.extend({
breadcrumbTitle: () => 'Configuration',
isBreadcrumbDropdownEnabled: () => false,
isDisplayingDatasets: () => false,
isDisplayingMaps: () => false,
isDisplayingLockedItems: () => false
});
@@ -0,0 +1,87 @@
const CoreView = require('backbone/core-view');
const randomQuote = require('builder/components/loading/random-quote');
const ServiceInvalidate = require('dashboard/data/service-invalidate-model');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const template = require('./service-disconnect-dialog.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'serviceModel',
'modalModel',
'configModel'
];
/**
* Disconnect service or help user to disconnect it
*
* - It needs the service model
*/
module.exports = CoreView.extend({
events: {
'click .js-revoke': '_revokeAccess',
'click .js-cancel': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
CoreView.prototype.initialize.apply(this);
this._initBinds();
},
render: function () {
if (this._serviceModel.get('state') === 'loading') {
return this.$el.html(
loadingTemplate({
title: 'Revoking access',
descHTML: randomQuote()
})
);
}
return this.$el.html(template(this._serviceModel.attributes));
},
_initBinds: function () {
this.listenTo(this._serviceModel, 'change:state', this._maybeReplaceContent);
},
_maybeReplaceContent: function () {
if (this._serviceModel.get('state') !== 'error') {
this.render();
}
},
_revokeAccess: function () {
const invalidateModel = new ServiceInvalidate({ datasource: this._serviceModel.get('name') });
invalidateModel._configModel = this._configModel;
this._serviceModel.set('state', 'loading');
invalidateModel.destroy({
success: (model, response) => {
if (response.success) {
this._reloadWindow();
} else {
this._setErrorState();
}
},
error: () => this._setErrorState()
});
},
_setErrorState: function () {
this._serviceModel.set('state', 'error');
this._closeDialog();
},
_reloadWindow: function () {
window.location.reload();
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,41 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-cloud"></i>
</div>
<p class="Dialog-headerTitle">
Disconnect your <%= title %> account
</p>
<p class="Dialog-headerText">
<% if (revoke_url) { %>
Revoke the access to CARTO
<% } else { %>
Are you sure you want to revoke the CARTO access to your <%= title %> account?
<% } %>
</p>
</div>
<% if (revoke_url) { %>
<div class="Dialog-body">
<p class="DefaultParagraph DefaultParagraph--short DefaultParagraph--centered DefaultParagraph--spaced">
We cant revoke the access for your <%= title %> account automatically.
</p>
<p class="DefaultParagraph DefaultParagraph--short DefaultParagraph--centered DefaultParagraph--spaced">
For your own security, we are unable to disconnect your <%= title %> account from CARTO. You can revoke access yourself by manually editing your <%= title %> authorized applications.
</p>
</div>
<% } %>
<div class="Dialog-footer u-inner">
<% if (revoke_url) { %>
<a href="<%- revoke_url%>" target="_blank" class="Button Button-inner Button--inline Button--secondary ">
<span>go to<%= title %></span>
</a>
<% } else { %>
<button class="CDB-Text Button Button--secondary Dialog-footerBtn Button--inline js-cancel">
<span>cancel</span>
</button>
<button class="CDB-Text js-revoke Button Button--negative Button--inline">
<span>Revoke access</span>
</button>
<% } %>
</div>
@@ -0,0 +1,20 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-keys"></i>
</div>
<p class="Dialog-headerTitle u-ellipsLongText">
You are about to delete your API key
</p>
<p class="Dialog-headerText">
All deployed apps with this API key will stop working. Are you sure you want to continue?
</p>
</div>
<div class="Dialog-footer u-inner">
<button type="button" class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button type="button" class="CDB-Button CDB-Button--error js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Delete API key</span>
</button>
</div>
@@ -0,0 +1,32 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
var REQUIRED_OPTS = [
'modalModel',
'onSubmit',
'template'
];
module.exports = CoreView.extend({
events: {
'click .js-submit': '_onSubmitClicked',
'click .js-cancel': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
return this.$el.html(this._template());
},
_onSubmitClicked: function () {
this._onSubmit();
this._closeDialog();
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,20 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-keys"></i>
</div>
<p class="Dialog-headerTitle u-ellipsLongText">
You are about to regenerate your API key
</p>
<p class="Dialog-headerText">
You will need to update all deployed apps with the new API key. Are you sure you want to continue?
</p>
</div>
<div class="Dialog-footer u-inner">
<button type="button" class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button type="button" class="CDB-Button CDB-Button--error js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Regenerate API key</span>
</button>
</div>
@@ -0,0 +1,287 @@
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
require('dashboard/components/form-components/index');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const ApiKeyModel = require('dashboard/data/api-key-model');
const TableGrantsView = require('dashboard/components/table-grants/table-grants-view');
const TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
const template = require('./api-keys-form.tpl');
const ApiKeysCollection = require('dashboard/data/api-keys-collection');
const REQUIRED_OPTS = [
'stackLayoutModel',
'userTablesModel',
'userModel'
];
const API_TYPES = {
maps: 'maps',
sql: 'sql'
};
const DATASET_SCOPE_TYPES = {
create: 'create',
listing: 'listing'
};
module.exports = CoreView.extend({
className: 'ApiKeysForm',
events: {
'click .js-back': '_onClickBack',
'click .js-submit': '_onFormSubmit',
'change input#create': '_onChangeCreateCheckbox',
'change input#sql': '_onChangeSqlCheckbox'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._apiKeysCollection = new ApiKeysCollection(null, { userModel: this._userModel });
this._apiKeyModel = options.apiKeyModel || new ApiKeyModel(null, { userModel: this._userModel });
this._formView = this._generateForm();
this.listenTo(this._formView, 'change', this._onFormChanged);
this.listenTo(this._apiKeyModel, 'change:tables', this._onFormChanged);
},
render: function () {
this.$el.empty();
this._initViews();
this._handleCheckboxState();
return this;
},
_initViews: function () {
this.$el.append(template({ modelIsNew: this._isNew() }));
this.$('.js-api-keys-form').append(this._formView.render().el);
this._tableGrantsView = new TableGrantsView({
apiKeyModel: this._apiKeyModel,
userTablesModel: this._userTablesModel
});
this.addView(this._tableGrantsView);
this.$('.js-api-keys-tables').append(this._tableGrantsView.render().el);
if (this._isNew()) {
this._renderTooltip();
}
},
_generateForm: function () {
const isDisabled = !this._isNew();
const schema = {
name: {
type: 'Text',
title: 'Name',
validators: ['required'],
hideValidationErrors: true,
editorAttrs: {
disabled: isDisabled,
placeholder: 'Your API key name',
id: 'js-api-key-name'
}
},
token: {
type: 'Text',
title: 'API Key',
hasCopyButton: isDisabled,
editorAttrs: {
disabled: true
}
},
apis: {
type: 'MultiCheckbox',
title: 'APIs',
validators: ['required'],
hideValidationErrors: true,
fieldClass: 'u-iBlock',
optional: true,
inputs: [
{ name: API_TYPES.sql, label: 'SQL' },
{ name: API_TYPES.maps, label: 'MAPS' }
],
editorAttrs: {
disabled: isDisabled
}
},
datasets: {
type: 'MultiCheckbox',
title: 'Datasets',
hideValidationErrors: true,
optional: true,
inputs: [
{ name: DATASET_SCOPE_TYPES.create, label: 'CREATE' },
{ name: DATASET_SCOPE_TYPES.listing, label: 'LISTING' }
],
editorAttrs: {
disabled: isDisabled
}
}
};
this._formView = new Backbone.Form({ model: this._apiKeyModel, schema });
return this._formView;
},
_isNew: function () {
return !this._apiKeyModel.get('id');
},
_onClickBack: function () {
this._stackLayoutModel.goToStep(0);
},
_renderTooltip: function () {
this._validationTooltip = new TipsyTooltipView({
el: this.$('.js-submit'),
gravity: 's',
title: () => 'Name and Datasets fields are required (Choose SQL or MAPS for specific permissions in selected datasets)'
});
this.addView(this._validationTooltip);
},
_hasErrors: function () {
const formErrors = this._formView.validate();
const selectedDatasetsPermissions = this._apiKeyModel.hasPermissionsSelected();
const selectedApis = _.some(this._formView.getValue().apis);
const selectedCreate = this._formView.getValue().datasets.create;
const selectedListing = this._formView.getValue().datasets.listing;
const selectedPermissions =
(selectedDatasetsPermissions && selectedApis) ||
selectedCreate ||
(selectedListing && !selectedApis);
return !!formErrors || !selectedPermissions;
},
_addApiKeyNameError: function (message) {
this._errorTooltip = new TipsyTooltipView({
el: this.$('#js-api-key-name'),
gravity: 'w',
title: () => message
});
this.addView(this._errorTooltip);
this._errorTooltip.showTipsy();
this.$('#js-api-key-name').addClass('has-error');
},
_onFormChanged: function () {
this._handleFormErrors();
this._handleCheckboxState();
this.$('.js-error').hide();
},
_onChangeCreateCheckbox: function () {
const createCheckbox = this._formView.getValue().datasets.create;
if (createCheckbox) {
const apisValues = _.clone(this._formView.getValue().apis);
apisValues.sql = createCheckbox;
this._formView.setValue('apis', apisValues);
this.$('input#sql').prop('checked', createCheckbox);
}
this._onFormChanged();
},
_onChangeSqlCheckbox: function () {
const SqlCheckbox = this._formView.getValue().apis.sql;
if (!SqlCheckbox) {
const datasetsValues = _.clone(this._formView.getValue().datasets);
datasetsValues.create = SqlCheckbox;
this._formView.setValue('datasets', datasetsValues);
this.$('input#create').prop('checked', SqlCheckbox);
}
this._onFormChanged();
},
_handleCheckboxState: function () {
const apis = this._isNew()
? this._formView.getValue().apis
: this._apiKeyModel.get('apis');
const activeApis = _.keys(apis).filter(name => apis[name]);
const disableCheckboxes = activeApis.length === 1 && _.contains(activeApis, API_TYPES.maps);
this.$('.ApiKeysForm-grantsTable').toggleClass('showOnlySelect', disableCheckboxes);
},
_handleFormErrors: function () {
const hasErrors = this._hasErrors();
this.$('.js-submit').toggleClass('is-disabled', hasErrors);
if (hasErrors) {
this._validationTooltip || this._renderTooltip();
} else {
this._validationTooltip && this._validationTooltip.clean();
this._validationTooltip = null;
}
this.$('#js-api-key-name').removeClass('has-error');
this._errorTooltip && this._errorTooltip.clean();
},
_onFormSubmit: function (event) {
const saveButton = this.$('.js-submit');
const isButtonLoading = saveButton.hasClass('is-loading');
if (this._hasErrors() || isButtonLoading) return;
saveButton.addClass('is-loading');
const errors = this._formView.commit({ validate: true });
if (errors) return;
this._apiKeysCollection.create(this._apiKeyModel.attributes, {
success: (model) => {
this._apiKeyModel = model;
this._generateForm();
this.render();
},
error: (model, request) => {
this._handleServerErrors(request.responseText);
saveButton.removeClass('is-loading');
},
userModel: this._userModel
});
},
_handleServerErrors: function (error) {
let message;
if (error.indexOf('Name has already been taken') !== -1) {
message = 'Name already exists';
} else if (error.indexOf('Name can\'t be blank') !== -1) {
message = 'Name can\'t be blank';
}
message && this._addApiKeyNameError(message);
if (!message) {
this._displayUnhandledError(error);
}
},
_displayUnhandledError: function (errorJSON) {
const parsed = JSON.parse(errorJSON);
this.$('.js-error').text(parsed.errors).show();
},
clean: function () {
this._userTablesModel.clearParams();
CoreView.prototype.clean.apply(this, arguments);
}
});
@@ -0,0 +1,34 @@
<section>
<header class="ApiKeysForm-title">
<button class="js-back">
<i class="CDB-IconFont CDB-IconFont-arrowPrev u-actionTextColor u-rSpace--xl"></i>
</button>
<h3 class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor">
<% if (modelIsNew) { %>
Configure your key
<% } else { %>
Your API key details
<% } %>
</h3>
</header>
<div class="js-api-keys-form"></div>
<div class="js-api-keys-tables"></div>
<footer class="Editor-footer u-tSpace-m">
<p class="CDB-Text CDB-Size-medium u-altTextColor">Changes to the key permissions are not possible once key is generated</p>
<% if (modelIsNew) { %>
<button type="submit" class="CDB-Button CDB-Button--primary CDB-Button--loading is-disabled js-submit">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Save changes</span>
<div class="CDB-Button-loader CDB-LoaderIcon is-white">
<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>
</button>
<% } %>
</footer>
<div class="CDB-Text CDB-Size-small u-errorTextColor js-error">
</div>
</section>
@@ -0,0 +1,11 @@
<section>
<h4 class="CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-actionTextColor">
<button class="js-edit"><%- name %></button>
</h4>
<p class="CDB-Text CDB-Size-medium u-secondaryTextColor u-tSpace--m u-flex u-alignCenter">
<span class="js-token"><%- token %></span>
<button class="js-copy">
<i class="CDB-IconFont CDB-IconFont-anchor u-hintTextColor CDB-Size-large u-lSpace--xl"></i>
</button>
</p>
</section>
@@ -0,0 +1,25 @@
<section class="u-width--100">
<h4 class="CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter">
<%- name %>
</h4>
<div class="u-flex u-alignCenter u-justifySpace u-tSpace--m">
<p class="CDB-Text CDB-Size-medium u-secondaryTextColor u-flex u-alignCenter">
<span class="js-token"><%- token %></span>
<button class="js-copy">
<i class="CDB-IconFont CDB-IconFont-anchor u-hintTextColor CDB-Size-large u-lSpace--xl"></i>
</button>
</p>
<ul class="u-flex">
<li>
<button class="CDB-Text CDB-Size-medium u-actionTextColor js-regenerate">Regenerate</button>
</li>
</ul>
</div>
<div class="u-tSpace">
<p class="ApiKeys-warning-text">
<span class="js-icon-warning ApiKeys-warning-icon"></span>
<span>For testing and development only! This API key can be used to perform any API request without restriction!</span>
</p>
</div>
</section>
@@ -0,0 +1,26 @@
<section>
<h4 class="CDB-Text CDB-Size-medium is-semibold u-flex u-alignCenter u-actionTextColor">
<button class="js-edit"><%- name %></button>
<% apiGrants.forEach(function (apiGrant) { %>
<span class="CDB-Tag CDB-Text is-gray is-semibold CDB-Size-small u-iBlock u-lSpace--xl u-upperCase">
<%- apiGrant %>
</span>
<% }) %>
</h4>
<p class="CDB-Text CDB-Size-medium u-secondaryTextColor u-tSpace--m u-flex u-alignCenter">
<span class="js-token"><%- token %></span>
<button class="js-copy">
<i class="CDB-IconFont CDB-IconFont-anchor u-hintTextColor CDB-Size-large u-lSpace--xl"></i>
</button>
</p>
</section>
<ul class="u-flex">
<li>
<button class="CDB-Text CDB-Size-medium u-actionTextColor u-rSpace--m js-regenerate">Regenerate</button>
</li>
<li>
<button class="CDB-Text CDB-Size-medium u-errorTextColor js-delete">Delete</button>
</li>
</ul>
@@ -0,0 +1,98 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const AlertDialogView = require('dashboard/views/api-keys/alert-dialog-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const templateDefault = require('./api-keys-list-item-default.tpl');
const templateMaster = require('./api-keys-list-item-master.tpl');
const templateRegular = require('./api-keys-list-item-regular.tpl');
const deleteKeyTemplate = require('./alert-delete-key.tpl');
const regenerateKeyTemplate = require('./alert-regenerate-key.tpl');
const IconView = require('builder/components/icon/icon-view');
const TEMPLATES = {
default: templateDefault,
master: templateMaster,
regular: templateRegular
};
const REQUIRED_OPTS = [
'apiKeyModel',
'onEdit'
];
module.exports = CoreView.extend({
tagName: 'li',
className: 'ApiKeys-list-item',
events: {
'click .js-edit': '_onItemClick',
'click .js-delete': '_onDeleteClick',
'click .js-regenerate': '_onRegenerateClick',
'click .js-copy': '_onCopyClick'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._apiKeyModel, 'change', this.render);
},
render: function () {
const template = TEMPLATES[this._apiKeyModel.get('type')];
this.$el.html(
template({
name: this._apiKeyModel.get('name'),
token: this._apiKeyModel.get('token'),
apiGrants: this._apiKeyModel.getApiGrants()
})
);
var warningIcon = new IconView({
placeholder: this.$el.find('.js-icon-warning'),
icon: 'warning'
});
warningIcon.render();
this.addView(warningIcon);
return this;
},
_onDeleteClick: function (event) {
const onSubmit = () => this._apiKeyModel.destroy();
this._modals.create(function (modalModel) {
return new AlertDialogView({ modalModel, onSubmit, template: deleteKeyTemplate });
});
},
_onRegenerateClick: function () {
const onSubmit = () => this._apiKeyModel.regenerate();
this._modals.create(function (modalModel) {
return new AlertDialogView({ modalModel, onSubmit, template: regenerateKeyTemplate });
});
},
_onCopyClick: function () {
const $temp = $('<input>');
const $token = this.$('.js-token');
$('body').append($temp);
$temp.val($token.text()).select();
document.execCommand('copy');
$temp.remove();
},
_onItemClick: function () {
this._onEdit(this._apiKeyModel);
}
});
@@ -0,0 +1,3 @@
<li class="ApiKeys-list-loader">
<div class="Spinner"></div>
</li>
@@ -0,0 +1,81 @@
const CoreView = require('backbone/core-view');
const PaginationView = require('builder/components/pagination/pagination-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./api-keys-list.tpl');
const ApiKeysListItemView = require('dashboard/views/api-keys/api-keys-list-item-view');
const loaderTemplate = require('./api-keys-list-loader.tpl');
const ApiKeysCollection = require('dashboard/data/api-keys-collection');
const REQUIRED_OPTS = [
'stackLayoutModel',
'userModel',
'apiKeysType',
'title',
'showNewApiKeyButton'
];
module.exports = CoreView.extend({
events: {
'click .js-add': '_onAddClick'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._apiKeysCollection = new ApiKeysCollection(null, { userModel: this._userModel, type: this._apiKeysType });
this._initBinds();
this._apiKeysCollection.fetch();
this._onEdit = this._onEdit.bind(this);
},
_initBinds: function () {
this.listenTo(this._apiKeysCollection, 'add change remove sync', this.render);
},
render: function () {
this.clearSubViews();
this.$el.html(template({
title: this._title,
showNewApiKeyButton: this._showNewApiKeyButton
}));
this._apiKeysCollection.status === 'fetched'
? this._renderKeys()
: this._renderLoading();
return this;
},
_renderLoading: function () {
this.$('.js-api-keys-list').append(loaderTemplate());
},
_renderKeys: function () {
this._apiKeysCollection.forEach(apiKeyModel => {
const view = new ApiKeysListItemView({
apiKeyModel,
onEdit: this._onEdit
});
this.addView(view);
this.$('.js-api-keys-list').append(view.render().el);
});
this.paginationView = new PaginationView({
model: this._apiKeysCollection.getPaginationModel()
});
this.addView(this.paginationView);
this.$('.js-api-keys-list').append(this.paginationView.render().el);
},
_onAddClick: function () {
this._stackLayoutModel.goToStep(1);
},
_onEdit: function (apiKeyModel) {
this._stackLayoutModel.goToStep(1, apiKeyModel);
}
});
@@ -0,0 +1,12 @@
<section>
<header class="ApiKeys-title">
<h3 class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%= title %></h3>
<% if (showNewApiKeyButton) { %>
<button type="submit" class="CDB-Button CDB-Button--primary js-add">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">New API key</span>
</button>
<% } %>
</header>
<ul class="ApiKeys-list js-api-keys-list"></ul>
</section>
@@ -0,0 +1,54 @@
const CoreView = require('backbone/core-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./api-keys-page.tpl');
const ApiKeysListView = require('dashboard/views/api-keys/api-keys-list-view');
const apiKeysCollectionTypes = require('dashboard/data/api-keys-collection-types');
const REQUIRED_OPTS = [
'stackLayoutModel',
'userModel'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
this.$el.html(template({
showGoogleApiKeys: this._userModel.showGoogleApiKeys(),
isInsideOrg: this._userModel.isInsideOrg(),
isOrgOwner: this._userModel.isOrgOwner(),
organizationName: this._userModel.getOrgName(),
googleApiKey: this._userModel.getGoogleApiKey()
}));
this._renderList(
[apiKeysCollectionTypes.MASTER, apiKeysCollectionTypes.DEFAULT].join(','),
'Default API Keys',
false
);
this._renderList(apiKeysCollectionTypes.REGULAR,
'Custom API Keys',
true
);
return this;
},
_renderList: function (apiKeysType, title, showNewApiKeyButton) {
const view = new ApiKeysListView({
stackLayoutModel: this._stackLayoutModel,
userModel: this._userModel,
apiKeysType: apiKeysType,
title,
showNewApiKeyButton
});
this.addView(view);
this.$('.js-api-keys-page').append(view.render().el);
}
});
@@ -0,0 +1,40 @@
<div class="js-api-keys-page"></div>
<% if (showGoogleApiKeys) { %>
<section>
<div class="FormAccount-title">
<p class="FormAccount-titleText">Configure API keys from external providers</p>
</div>
<span class="FormAccount-separator"></span>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor FormAccount-label">Google Maps</label>
</div>
<div class="FormAccount-rowData">
<input type="text" value="<%- googleApiKey %>" class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--long is-disabled" readonly />
</div>
<div class="FormAccount-rowInfo">
<% if (!isInsideOrg) { %>
<p class="CDB-Text CDB-Size-small u-altTextColor">
This is your Google Maps query string, contact with <a href="mailto:support@carto.com">support@carto.com</a> to change it.
</p>
<% } else if (isOrgOwner) { %>
<p class="CDB-Text CDB-Size-small u-altTextColor">
This is the <%= organizationName %> Google Maps query string, contact with <a href="mailto:support@carto.com">support@carto.com</a> to change it.
</p>
<% } else { %>
<p class="CDB-Text CDB-Size-small u-altTextColor">This is the organization Google Maps API key</p>
<% } %>
</div>
</div>
</section>
<% } %>
<footer class="ApiKeys-footer">
<p class="ApiKeys-warning-text">
<i class="CDB-IconFont CDB-IconFont-info ApiKeys-info-icon"></i>
<span class="u-altTextColor">Learn more about location app authorization and API key management <a href="https://carto.com/developers/fundamentals/authorization/" target="_blank">here</a></span>
</p>
</footer>
@@ -0,0 +1,17 @@
const Backbone = require('backbone');
/**
* Header view model to handle state for dashboard header view.
*/
module.exports = Backbone.Model.extend({
breadcrumbTitle: () => 'Configuration',
isBreadcrumbDropdownEnabled: () => false,
isDisplayingDatasets: () => false,
isDisplayingMaps: () => false,
isDisplayingLockedItems: () => false
});
@@ -0,0 +1,34 @@
const CoreView = require('backbone/core-view');
const template = require('./background-polling-header-title.tpl');
/**
* Background polling header title view
*
* It will contain only the title
*
*/
module.exports = CoreView.extend({
tagName: 'h3',
className: 'CDB-Text CDB-Size-large u-lSpace--xl',
initialize: function () {
this._initBinds();
},
render: function () {
this.$el.html(
template({
imports: this.model.getTotalImports(),
totalPollings: this.model.getTotalPollings()
})
);
return this;
},
_initBinds: function () {
this.model.bind('change analysisAdded analysisRemoved importAdded importRemoved geocodingAdded geocodingRemoved', this.render, this);
}
});
@@ -0,0 +1,8 @@
<% if (totalPollings === 1) { %>
<% if (imports > 0) { %>
Connecting
<% } %>
dataset...
<% } else { %>
Working...
<% } %>
@@ -0,0 +1,57 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const BackgroundPollingHeaderTitleView = require('dashboard/views/dashboard/background-polling/background-polling-header-title/background-polling-header-title-view');
const template = require('./background-polling-header.tpl');
/**
* Background polling header view
*
* It will contain:
* - Badge
* - Title
*
*/
module.exports = CoreView.extend({
className: 'BackgroundPolling-header',
initialize: function () {
this._initBinds();
},
render: function () {
this.$el.html(template());
this._initViews();
this._updateBadges();
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change importAdded importRemoved geocodingAdded geocodingRemoved', this._updateBadges);
},
_initViews: function () {
const headerTitle = new BackgroundPollingHeaderTitleView({
model: this.model
});
this.$el.append(headerTitle.render().el);
this.addView(headerTitle);
},
_updateBadges: function () {
const failed = this.model.getTotalFailedItems();
if (this.$('.BackgroundPolling-headerBadgeCount').length === 0 && failed > 0) {
const $span = $('<span>').addClass('BackgroundPolling-headerBadgeCount Badge Badge--negative CDB-Text CDB-Size-small').text(failed);
this.$('.BackgroundPolling-headerBadge')
.append($span)
.addClass('has-failures');
} else if (this.$('.BackgroundPolling-headerBadgeCount').length > 0 && failed > 0) {
this.$('.BackgroundPolling-headerBadgeCount').text(failed);
} else if (failed === 0) {
this.$('.BackgroundPolling-headerBadgeCount').remove();
this.$('.BackgroundPolling-headerBadge').removeClass('has-failures');
}
}
});
@@ -0,0 +1,3 @@
<div class="BackgroundPolling-headerBadge LayoutIcon">
<i class="CDB-IconFont CDB-IconFont-cloud BackgroundPolling-headerBadgeIcon js-icon"></i>
</div>
@@ -0,0 +1,166 @@
const CoreView = require('backbone/core-view');
const ImportItemView = require('dashboard/views/dashboard/imports/background-import-item/background-import-item-view');
const ImportLimitItemView = require('builder/components/background-importer/background-import-limit-view');
const ImportsModel = require('dashboard/data/imports-model');
const BackgroundPollingModel = require('dashboard/data/background-polling/background-polling-model');
const BackgroundPollingHeaderView = require('dashboard/views/dashboard/background-polling/background-polling-header/background-polling-header-view');
const template = require('./background-polling.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel',
'userModel',
'createVis'
];
/**
* Background polling view
*
* It will pool all polling operations that happens
* in Cartodb, as in imports and geocodings
*
*/
module.exports = CoreView.extend({
className: 'BackgroundPolling',
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._vis = options.vis;
if (!this.model) {
this.model = new BackgroundPollingModel({}, {
userModel: this._userModel
});
}
this._initBinds();
},
render: function () {
this.$el.html(template());
this._initViews();
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'importAdded', this._addImport);
this.listenTo(this.model, 'importAdded importRemoved', this._checkPollingsSize);
},
_initViews: function () {
const backgroundPollingHeaderView = new BackgroundPollingHeaderView({
model: this.model
});
this.$el.prepend(backgroundPollingHeaderView.render().el);
this.addView(backgroundPollingHeaderView);
},
_checkPollingsSize: function () {
if (this.model.getTotalPollings() > 0) {
this.show();
} else {
this.hide();
}
},
_addImport: function (model) {
const importItem = new ImportItemView({
showSuccessDetailsButton: this.model.get('showSuccessDetailsButton'),
model,
userModel: this._userModel,
configModel: this._configModel
});
importItem.bind('remove', function (mdl) {
this.model.removeImportItem(mdl);
}, this);
this.$('.js-list').prepend(importItem.render().el);
this.addView(importItem);
this.enable();
},
_addDataset: function (d) {
if (d) {
this._addImportsItem(d);
}
},
_onDroppedFile: function (files) {
if (files) {
this._addImportsItem({
type: 'file',
value: files,
create_vis: files._createVis || this._createVis
});
}
},
_addImportsItem: function (uploadData) {
if (this.model.canAddImport()) {
this._removeLimitItem();
} else {
this._addLimitItem();
return false;
}
const imp = new ImportsModel({}, {
upload: uploadData,
userModel: this._userModel,
configModel: this._configModel
});
this.model.addImportItem(imp);
},
// Limits view
_addLimitItem: function () {
if (!this._importLimit) {
const view = new ImportLimitItemView({
userModel: this._userModel,
configModel: this._configModel
});
this.$('.js-list').prepend(view.render().el);
this.addView(view);
this._importLimit = view;
}
},
_removeLimitItem: function () {
var view = this._importLimit;
if (view) {
view.clean();
this.removeView(view);
delete this._importLimit;
}
},
// Enable background polling checking
// ongoing imports
enable: function () {
this.model.startPollings();
},
// Disable/stop background pollings
disable: function () {
this.model.stopPollings();
},
show: function () {
this.$el.addClass('is-visible');
},
hide: function () {
this.$el.removeClass('is-visible');
},
clean: function () {
this.disable();
CoreView.prototype.clean.apply(this);
}
});
@@ -0,0 +1,3 @@
<div class="BackgroundPolling-body CDB-Text CDB-Size-medium">
<ul class="BackgroundPolling-list js-list"></ul>
</div>
@@ -0,0 +1,247 @@
const Backbone = require('backbone');
const UploadModel = require('dashboard/data/upload-model');
const VisFetchModel = require('builder/data/visualizations-fetch-model');
const VisualizationsCollection = require('dashboard/data/visualizations-collection');
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
const IMPORT = 'import';
const DATASETS = 'datasets';
const SCRATCH = 'scratch';
const IMPORT_TWITTER = 'listing.import.twitter';
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'configModel',
'backgroundPollingView'
];
/**
* Create dataset model
*
* - Store the state of the dialog (listing or loading).
* - Store the selected datasets for a map creation.
* - Store the upload info for a dataset creation.
*/
module.exports = Backbone.Model.extend({
defaults: {
type: 'dataset',
option: 'listing',
listing: 'datasets',
contentPane: 'listing' // [import, datasets, scratch]
},
initialize: function (attributes, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.upload = new UploadModel(
{ create_vis: false },
{ userModel: this._userModel,
configModel: this._configModel }
);
this.selectedDatasets = new Backbone.Collection();
this.collection = new VisualizationsCollection(null, { configModel: this._configModel });
this.visFetchModel = new VisFetchModel({
content_type: 'datasets',
library: this.showLibrary()
});
this._initBinds();
},
viewsReady: function () {
// nothing to do for this use-case
this.set('listing', 'import');
},
// For create-listing view
showLibrary: function () {
return true;
},
// For create-listing view
showDatasets: function () {
return false;
},
// For create-listing view
canSelect: function () {
return true;
},
// Get option state (it could be loading or listing)
getOption: function () {
const option = this.get('option');
const states = option.split('.');
if (states.length > 0) {
return states[0];
}
return '';
},
// Get import state (it could be any of the possibilities of the import options, as in scratch, dropbox, etc...)
// For create-footer view
getImportState: function () {
const option = this.get('option');
const states = option.split('.');
if (states.length > 0 && states.length < 4 && states[0] === 'listing' && states[1] === 'import') {
return states[2];
}
return '';
},
// For create-footer view
showGuessingToggler: function () {
return true;
},
// For create-footer view
showPrivacyToggler: function () {
const hiddenDueToDeprecation = this._atTwitterImportPane() && !this._userModel.hasOwnTwitterCredentials();
const hasToBeShowed = this._atImportPane() && !hiddenDueToDeprecation;
return hasToBeShowed;
},
_atImportPane: function () {
return this.get('listing') === IMPORT;
},
_atDatasetsPane: function () {
return this.get('listing') === DATASETS;
},
_atScratchPane: function () {
return this.get('listing') === SCRATCH;
},
_atTwitterImportPane: function () {
return this.get('option') === IMPORT_TWITTER;
},
// For create-footer view
startUpload: function () {
this._backgroundPollingView._addDataset(this.upload.toJSON());
},
// For create-listing-import view
setActiveImportPane: function (option) {
if (option && this._atImportPane() && this.getImportState() !== option) {
this.set('option', 'listing.import.' + option);
}
},
// For create-footer view
isMapType: function () {
return false;
},
// For create-from-scratch view
createFromScratch: function () {
this.trigger('creatingDataset', 'dataset', this);
this.set('contentPane', 'creatingFromScratch');
var dataset = new CartoTableMetadata(null, { configModel: this._configModel });
dataset.save({}, {
success: m => {
this.trigger('datasetCreated', m, this);
},
error: (m, e) => {
this.trigger('datasetError', e, this);
}
});
},
_initBinds: function () {
this.listenTo(this.upload, 'change', function () {
this.trigger('change:upload', this);
});
this.listenTo(this.collection, 'change:selected', this._onItemSelected);
this.listenTo(this.visFetchModel, 'change', this._fetchCollection);
this.listenTo(this, 'change:option', this._maybePrefetchDatasets, this);
this.listenTo(this, 'change:listing', this._maybePrefetchDatasets, this);
},
_maybePrefetchDatasets: function () {
const isDatasets = this.get('listing') === 'datasets';
// Fetch collection if it was never fetched (and a search is not applied!)
if (isDatasets && !this.get('collectionFetched') && !this.visFetchModel.isSearching()) {
this.set('collectionFetched', true);
this._fetchCollection();
}
},
getVisualizationFetchModel: function () {
return this.visFetchModel;
},
getTablesCollection: function () {
return this.collection;
},
getSelectedDatasetsCollection: function () {
return this.selectedDatasets;
},
getUploadModel: function () {
return this.upload;
},
canFinish: function () {},
finish: function () {},
_selectedItems: function () {
return this.collection.where({ selected: true });
},
_fetchCollection: function () {
const params = this.visFetchModel.attributes;
this.collection.options.set({
locked: '',
q: params.q,
page: params.page || 1,
tags: params.tag,
per_page: this.collection['_TABLES_PER_PAGE'],
shared: params.shared,
only_liked: params.liked,
order: 'updated_at',
type: '',
types: params.library ? 'remote' : 'table',
exclude_raster: true
});
this.collection.fetch();
},
_onItemSelected: function (changedModel) {
// Triggers an import immediately
if (changedModel.get('type') === 'remote') {
// previously located in listings/datasets/remote_datasets_item_view
const table = new CartoTableMetadata(changedModel.get('external_source'), { configModel: this._configModel });
const data = {
type: 'remote',
value: changedModel.get('name'),
remote_visualization_id: changedModel.get('id'),
size: table.get('size'),
create_vis: false
};
this._backgroundPollingView._addDataset(data);
this.trigger('destroyModal');
}
}
});
@@ -0,0 +1,385 @@
const Backbone = require('backbone');
const _ = require('underscore');
const ImportsModel = require('builder/data/background-importer/imports-model');
const UploadModel = require('dashboard/data/upload-model');
const PermissionModel = require('dashboard/data/permission-model');
const VisualizationModel = require('dashboard/data/visualization-model');
const VisualizationsCollection = require('dashboard/data/visualizations-collection');
const VisFetchModel = require('builder/data/visualizations-fetch-model');
const CartoTableMetadata = require('dashboard/views/public-dataset/carto-table-metadata');
const TablesCollection = require('builder/data/visualizations-collection');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const IMPORT = 'import';
const DATASETS = 'datasets';
const SCRATCH = 'scratch';
const IMPORT_TWITTER = 'listing.import.twitter';
const REQUIRED_OPTS = [
'userModel',
'configModel',
'backgroundPollingView'
];
/**
* This model will be on charge of create a new map
* using user selected datasets, where they can be
* already imported datasets or remote (and needed to import)
* datasets.
*/
module.exports = Backbone.Model.extend({
defaults: {
type: 'map',
contentPane: 'listing', // [listing, loading]
option: 'listing',
currentImport: null,
tableIdsArray: [],
listing: 'datasets', // [import, datasets, scratch]
collectionFetched: false,
activeImportPane: 'file'
},
_DEFAULT_MAP_NAME: 'Untitled Map',
initialize: function (attributes, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.upload = new UploadModel(
{ create_vis: true },
{ userModel: this._userModel, configModel: this._configModel }
);
this.selectedDatasets = new TablesCollection(options.selectedItems, { configModel: this._configModel });
this.collection = new VisualizationsCollection(null, { configModel: this._configModel });
this.vis = new VisualizationModel({ name: 'Untitled map' }, { configModel: this._configModel });
this.visFetchModel = new VisFetchModel({
content_type: 'datasets',
library: this.showLibrary()
});
this._initBinds();
},
setSelected: function (datasets) {
this.selectedDatasets.reset(datasets);
},
// For entry point, notifies model that depending views are ready for changes (required for custom events)
viewsReady: function () {
if (this.selectedDatasets.isEmpty()) {
this._maybePrefetchDatasets();
} else {
// Not empty, so start creating map from these preselected items
this.createMap();
}
},
// For create-listing view
showLibrary: function () {
return false;
},
// For create-listing view
showDatasets: function () {
return true;
},
// For create-listing view
canSelect: function (datasetModel) {
if (datasetModel.get('selected')) {
return true;
} else {
return this.selectedDatasets.length < this._userModel.getMaxLayers();
}
},
canFinish: function () {
if (this.get('listing') === 'import') {
return this.upload.isValidToUpload();
} else if (this.get('listing') === 'datasets') {
return this.selectedDatasets.length > 0;
}
},
finish: function () {},
// Get option state (it could be listing or loading)
getOption: function () {
var option = this.get('option');
var states = option.split('.');
if (states.length > 0) {
return states[0];
}
return '';
},
// Get import state (it could be any of the possibilities of the import options, as in scratch, dropbox, etc...)
// For create-footer view
getImportState: function () {
var option = this.get('option');
var states = option.split('.');
if (states.length > 0 && states.length < 4 && states[0] === 'listing' && states[1] === 'import') {
return states[2];
}
return '';
},
// For create-footer view
showGuessingToggler: function () {
return true;
},
// For create-footer view
showPrivacyToggler: function () {
var hiddenDueToDeprecation = this._atTwitterImportPane() && !this._userModel.hasOwnTwitterCredentials();
var hasToBeShowed = this._atImportPane() && !hiddenDueToDeprecation;
return hasToBeShowed;
},
// For create-listing-import view
setActiveImportPane: function (option) {
if (option && this._atImportPane() && this.getImportState() !== option) {
this.set('option', 'listing.import.' + option);
}
},
// For create-footer view
isMapType: function () {
return true;
},
// For create-footer view
startUpload: function () {
this._backgroundPollingView._addDataset(this.upload.toJSON());
},
createMap: function () {
if (this.selectedDatasets.length === 0) {
return;
}
this.set('contentPane', 'loading');
this._checkCollection();
},
// For create-from-scratch view
createFromScratch: function (configModel) {
this.trigger('creatingDataset', 'dataset', this);
this.set('contentPane', 'loading');
var dataset = new CartoTableMetadata(null, { configModel: this._configModel });
dataset.save({}, {
success: m => {
this.trigger('datasetCreated', m, this);
},
error: (m, e) => {
this.trigger('datasetError', e, this);
}
});
},
_initBinds: function () {
this.upload.bind('change', function () {
this.trigger('change:upload', this);
}, this);
this.bind('change:option', this._onOptionChange, this);
this.collection.bind('change:selected', function (changedModel, wasSelected) {
this.selectedDatasets[ wasSelected ? 'add' : 'remove' ](changedModel);
}, this);
this.collection.bind('reset', function () {
this.selectedDatasets.each(function (model) {
var sameModel = this.collection.get(model.id);
if (sameModel) {
sameModel.set('selected', true);
}
}, this);
}, this);
this.visFetchModel.bind('change', this._fetchCollection, this);
if (this.selectedDatasets.isEmpty()) {
this.bind('change:option', this._maybePrefetchDatasets, this);
this.bind('change:listing', this._maybePrefetchDatasets, this);
}
},
_maybePrefetchDatasets: function () {
var isDatasets = this.get('listing') === 'datasets';
// Fetch collection if it was never fetched (and a search is not applied!)
if (isDatasets && !this.get('collectionFetched') && !this.visFetchModel.isSearching()) {
this._fetchCollection();
}
},
_fetchCollection: function () {
this.set('collectionFetching', true);
var params = this.visFetchModel.attributes;
var types;
if (this.visFetchModel.isSearching()) {
// Supporting search in data library and user datasets at the same time
types = 'table,remote';
} else {
types = params.library ? 'remote' : 'table';
}
this.collection.options.set({
locked: '',
q: params.q,
page: params.page || 1,
tags: params.tag,
per_page: this.collection['_TABLES_PER_PAGE'],
shared: params.shared,
only_liked: params.liked,
order: 'updated_at',
type: '',
types: types,
exclude_raster: true
});
this.collection.fetch({
success: function () {
this.set({
collectionFetching: false,
collectionFetched: true
});
}.bind(this)
});
},
getVisualizationFetchModel: function () {
return this.visFetchModel;
},
getTablesCollection: function () {
return this.collection;
},
getSelectedDatasetsCollection: function () {
return this.selectedDatasets;
},
getUploadModel: function () {
return this.upload;
},
_selectedItems: function () {
return this.selectedDatasets;
},
_checkCollection: function () {
if (this.selectedDatasets.length > 0) {
this._importDataset(this.selectedDatasets.pop());
} else {
this.set('currentImport', '');
this._createMap();
}
},
_importDataset: function (mdl) {
var tableIdsArray = _.clone(this.get('tableIdsArray'));
if (mdl.get('type') === 'remote') {
var d = {
create_vis: false,
type: 'remote',
value: mdl.get('name'),
remote_visualization_id: mdl.get('id'),
size: mdl.get('external_source') ? mdl.get('external_source').size : undefined
};
var impModel = new ImportsModel({}, {
upload: d,
userModel: this._userModel,
configModel: this._configModel
});
this.set('currentImport', _.clone(impModel));
this.trigger('importingRemote', this);
impModel.bind('change:state', function (m) {
if (m.hasCompleted()) {
var data = m.getImportModel().toJSON();
tableIdsArray.push(data.table_name);
this.set('tableIdsArray', tableIdsArray);
this._checkCollection();
this.trigger('importCompleted', this);
}
if (m.hasFailed()) {
this.set('contentPane', 'importFailed');
this.trigger('importFailed', this);
}
}, this);
// If import model has any errors at the beginning
if (impModel.hasFailed()) {
this.set('contentPane', 'importFailed');
this.trigger('importFailed', this);
}
} else {
var table = mdl.tableMetadata();
tableIdsArray.push(table.get('name'));
this.set({
currentImport: '',
tableIdsArray: tableIdsArray
});
this._checkCollection();
}
},
_createMap: function () {
const vis = new VisualizationModel({
name: this._DEFAULT_MAP_NAME,
type: 'derived'
}, { configModel: this._configModel });
vis.permission = new PermissionModel({
owner: this._userModel.attributes
}, {
configModel: this._configModel,
userModel: this._userModel
});
this.trigger('creatingMap', this);
vis.save({
tables: this.get('tableIdsArray')
}, {
success: () => {
this._redirectTo(vis.viewUrl(this._userModel).edit().toString());
},
error: () => {
this.trigger('mapError', this);
}
});
},
_atImportPane: function () {
return this.get('listing') === IMPORT;
},
_atDatasetsPane: function () {
return this.get('listing') === DATASETS;
},
_atScratchPane: function () {
return this.get('listing') === SCRATCH;
},
_atTwitterImportPane: function () {
return this.get('option') === IMPORT_TWITTER;
},
_redirectTo: function (url) {
window.location = url;
}
});
@@ -0,0 +1,60 @@
const Backbone = require('backbone');
const batchProcessItems = require('dashboard/helpers/batch-process-items');
/**
* View model for change lock view.
* Manages the life cycle states for the change lock view.
*/
module.exports = Backbone.Model.extend({
defaults: {
state: 'ConfirmChangeLock',
initialLockValue: false,
contentType: 'datasets',
items: undefined // a Backbone collection
},
initialize: function (attributes) {
this.set('items', new Backbone.Collection(attributes.items));
const lockedStates = this.get('items').chain()
.map(item => item.get('locked'))
.uniq()
.value()
.length;
if (lockedStates > 1) {
const errorMsg = 'It is assumed that all items have the same locked state, a user should never be able to ' +
'select a mixed item with current UI. If you get an error with this message something is broken';
if (window.trackJs && window.trackJs.track) {
window.trackJs.track(errorMsg);
} else {
throw new Error(errorMsg);
}
}
this.set('initialLockValue', this.get('items').at(0).get('locked'));
},
inverseLock: function () {
this.set('state', 'ProcessingItems');
batchProcessItems({
howManyInParallel: 5,
items: this.get('items').toArray(),
processItem: this._lockItem.bind(this, !this.get('initialLockValue')),
done: this.set.bind(this, 'state', 'ProcessItemsDone'),
fail: this.set.bind(this, 'state', 'ProcessItemsFail')
});
},
_lockItem: function (newLockedValue, item, callback) {
item.save({ locked: newLockedValue })
.done(function () {
callback();
})
.fail(() =>
callback('something failed') // eslint-disable-line
);
}
});
@@ -0,0 +1,84 @@
const CoreView = require('backbone/core-view');
const pluralizeString = require('dashboard/helpers/pluralize');
const loadingView = require('builder/components/loading/render-loading');
const failTemplate = require('dashboard/components/fail.tpl');
const template = require('./change-lock.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'modalModel'
];
/**
* Lock/unlock datasets/maps dialog.
*/
module.exports = CoreView.extend({
events: {
'click .js-ok': '_ok',
'click .js-cancel': 'close'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this.listenTo(this.model, 'change', function () {
if (this.model.get('state') === 'ProcessItemsDone') {
this.close();
} else {
this.render();
}
});
},
render: function () {
this.$el.html(this['_render' + this.model.get('state')]());
},
_renderConfirmChangeLock: function () {
// An entity can be an User or Organization
const itemsCount = this.model.get('items').length;
const areLocked = this.model.get('initialLockValue');
const viewTemplate = this.options.template || template;
return viewTemplate({
model: this.model,
itemsCount: itemsCount,
ownerName: this.options.ownerName,
isOwner: this.options.isOwner,
thisOrTheseStr: itemsCount === 1 ? 'this' : 'these',
itOrThemStr: itemsCount === 1 ? 'it' : 'them',
areLocked: areLocked,
positiveOrNegativeStr: areLocked ? 'positive' : 'alert',
lockOrUnlockStr: areLocked ? 'unlock' : 'lock',
contentTypePluralized: pluralizeString(
this.model.get('contentType') === 'datasets' ? 'dataset' : 'map', // singular
this.model.get('contentType'), // plural
itemsCount
)
});
},
_ok: function (e) {
this.killEvent(e);
this.model.inverseLock();
this.render();
},
close: function () {
this._modalModel.destroy();
},
_renderProcessingItems: function () {
const lockingOrUnlockingStr = this.model.get('initialLockValue') ? 'Unlocking' : 'Locking';
return loadingView({
title: `${lockingOrUnlockingStr} ${pluralizeString(this.model.get('contentType') === 'datasets' ? 'dataset' : 'map', this.model.get('items').length)}`
});
},
_renderProcessItemsFail: function () {
var lockingOrUnlockingStr = this.model.get('initialLockValue') ? 'unlock' : 'lock';
return failTemplate({
msg: 'Failed to ' + lockingOrUnlockingStr + ' all items'
});
}
});
@@ -0,0 +1,34 @@
<div class="Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--<%- positiveOrNegativeStr %>">
<i class="CDB-IconFont <%- areLocked ? 'CDB-IconFont-unlock' : 'CDB-IconFont-lock' %>"></i>
<% if (itemsCount > 1) { %>
<span class="Badge Badge--<%- positiveOrNegativeStr %> Dialog-headerIconBadge CDB-Text CDB-Size-small "><%- itemsCount %></span>
<% } %>
</div>
<h3 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
You are about to <%- lockOrUnlockStr %> <%- itemsCount %> <%- contentTypePluralized %>.
</h3>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (areLocked) { %>
<%- _t('components.modals.change-lock.description.locked', {
thisOrTheseStr: thisOrTheseStr,
contentTypePluralized: contentTypePluralized,
itOrThemStr: itOrThemStr
}) %>
<% } else { %>
<%- _t('components.modals.change-lock.description.unlocked', {
thisOrTheseStr: thisOrTheseStr,
contentTypePluralized: contentTypePluralized,
itOrThemStr: itOrThemStr
}) %>
<% } %>
</p>
</div>
<div class="Dialog-footer Dialog-footer--simple u-inner">
<button class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">cancel</span>
</button>
<button class="CDB-Button CDB-Button--primary CDB-Button--<%- positiveOrNegativeStr %> u-lSpace--xl js-ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Ok, <%- lockOrUnlockStr %></span>
</button>
</div>
@@ -0,0 +1,194 @@
const CoreView = require('backbone/core-view');
const TabPane = require('dashboard/components/tabpane/tabpane');
const StartView = require('./start-view');
const PrivacyOptions = require('./options-collection');
const loadingView = require('builder/components/loading/render-loading');
const failTemplate = require('dashboard/components/fail.tpl');
const ViewFactory = require('builder/components/view-factory');
const MapcapsCollection = require('builder/data/mapcaps-collection');
const PrivacyWarningView = require('builder/components/modals/privacy-warning/privacy-warning-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const VisualizationModel = require('dashboard/data/visualization-model');
const ShareView = require('dashboard/views/dashboard/dialogs/share/share-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const upgradeErrorTemplate = require('builder/components/background-importer/upgrade-errors.tpl');
const REQUIRED_OPTS = [
'userModel',
'visModel',
'configModel',
'modals',
'modalModel'
];
/**
* Change privacy datasets/maps dialog.
*/
const ChangePrivacyView = CoreView.extend({
events: {
'click .ok': 'ok',
'click .cancel': 'destroyDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._privacyOptions = PrivacyOptions.byVisAndUser(this._visModel, this._userModel);
this._privacyModal = new ModalsServiceModel();
this._initViews();
this._initBinds();
},
render: function () {
return this._panes.getActivePane().render().el;
},
ok: function () {
const selectedOption = this._privacyOptions.selectedOption();
if (!selectedOption.canSave()) {
return;
}
return this._shouldShowPrivacyWarning(selectedOption.get('privacy'))
.then(shouldShowWarning => {
this._panes.active('saving');
if (shouldShowWarning) {
this._checkPrivacyChange(
selectedOption.get('privacy'),
() => this._savePrivacy(selectedOption),
() => this._panes.active('start')
);
} else {
this._savePrivacy(selectedOption);
}
});
},
destroyDialog: function () {
this._modalModel.destroy();
},
_initViews: function () {
this._panes = new TabPane({
el: this.el
});
this.addView(this._panes);
this._panes.addTab('start',
new StartView({
privacyOptions: this._privacyOptions,
userModel: this._userModel,
visModel: this._visModel,
configModel: this._configModel
})
);
this._panes.addTab('saving',
ViewFactory.createByHTML(loadingView({
title: 'Saving privacy…'
}))
);
this._panes.addTab('saveFail',
ViewFactory.createByHTML(failTemplate({
msg: ''
}))
);
const upgradeUrl = this._visModel._configModel.get('upgrade_url');
const userCanUpgrade = upgradeUrl && !this._visModel._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
const upgradeHtml = upgradeErrorTemplate({
errorCode: 8007,
userCanUpgrade: userCanUpgrade,
showTrial: this._userModel.canStartTrial(),
upgradeUrl: upgradeUrl
});
this._panes.addTab('upgrade',
ViewFactory.createByHTML(upgradeHtml)
);
this._panes.active('start');
},
_initBinds: function () {
this._panes.bind('tabEnabled', this.render, this);
this._panes.getPane('start').bind('clickedShare', this._openShareDialog, this);
},
_openShareDialog: function () {
this._modals.create(modalModel => {
// Order matters, close this dialog before appending the share one, for side-effects to work as expected (body.is-inDialog)
return new ShareView({
configModel: this._configModel,
userModel: this._userModel,
visModel: this._visModel,
modals: this._modals,
modalModel,
onClose: this._onShareClose
});
});
},
_onShareClose: function () {
this._modals.create(modalModel => {
return new ChangePrivacyView({
visModel: this._visModel,
userModel: this._userModel,
configModel: this._configModel,
modals: this._modals,
modalModel
});
});
},
_shouldShowPrivacyWarning: function (privacyState) {
if (!this._userModel.canSelectPremiumOptions(this._visModel)) {
return Promise.resolve(false);
}
const isPubliclyAvailable = VisualizationModel.isPubliclyAvailable(privacyState);
if (this._visModel.isVisualization()) {
this._panes.active('saving');
const mapcapsCollection = new MapcapsCollection(null, {
visDefinitionModel: this._visModel
});
return new Promise((resolve, reject) => {
mapcapsCollection.fetch({
success: () => {
resolve(!!mapcapsCollection.length && isPubliclyAvailable);
},
error: reject
});
});
}
return Promise.resolve(isPubliclyAvailable);
},
_savePrivacy: function (privacyOption) {
privacyOption.saveToVis(this._visModel, {
success: () => {
this._modalModel.destroy();
},
error: (req, resp) => {
if (resp.responseText.indexOf('over account public map quota') !== -1) {
this._panes.active('upgrade');
} else {
this._panes.active('saveFail');
}
}
});
},
_checkPrivacyChange: function (newPrivacyState, confirmCallback, dismissCallback) {
this._privacyModal.create(modalModel => {
return new PrivacyWarningView({
modalModel: modalModel,
privacyType: newPrivacyState,
type: this._visModel.isVisualization() ? 'visualization' : 'dataset',
onConfirm: confirmCallback,
onDismiss: dismissCallback
});
});
}
});
module.exports = ChangePrivacyView;
@@ -0,0 +1,47 @@
const _ = require('underscore');
const Backbone = require('backbone');
/**
* Default model for a privacy option.
*/
module.exports = Backbone.Model.extend({
defaults: {
privacy: 'PUBLIC',
disabled: false,
selected: false,
password: undefined
},
validate: function (attrs) {
if (attrs.disabled && attrs.selected) {
return 'Option can not be disabled and selected at the same time';
}
},
classNames: function () {
return _.chain(['disabled', 'selected'])
.map(attr => this.attributes[attr] ? 'is-' + attr : undefined)
.compact().value().join(' ');
},
canSave: function () {
return !this.get('disabled');
},
/**
* @param vis {Object} instance of cdb.admin.Visualization
* @param callbacks {Object}
*/
saveToVis: function (vis, callbacks) {
return vis.save(this._attrsToSave(), _.extend({ wait: true }, callbacks));
},
/**
* @returns {Object} attrs
* @protected
*/
_attrsToSave: function () {
return _.pick(this.attributes, 'privacy', 'password');
}
});
@@ -0,0 +1,104 @@
const Backbone = require('backbone');
const _ = require('underscore');
const OptionModel = require('./option-model');
const PasswordOptionModel = require('./password-option-model');
/**
* type property should match the value given from the API.
*/
const ALL_OPTIONS = [{
privacy: 'PUBLIC',
illustrationType: 'positive',
iconFontType: 'unlock',
title: 'Public',
desc: '任何人都可以查找和查看.',
alwaysEnable: true
}, {
privacy: 'LINK',
illustrationType: 'alert',
iconFontType: 'unlock',
title: 'Public - With Link',
desc: '知道链接的任何人都可以查看,不需要密码.'
}, {
privacy: 'PASSWORD',
illustrationType: 'alert',
iconFontType: 'unlockWithEllipsis',
title: 'Public - With Password',
desc: '拥有密码的任何人都可以查看.'
}, {
privacy: 'PRIVATE',
illustrationType: 'negative',
iconFontType: 'lock',
title: 'Private',
desc: '只有您可以访问.'
}];
/**
* Collection that holds the different privacy options.
*/
module.exports = Backbone.Collection.extend({
model: function (attrs, options) {
if (attrs.privacy === 'PASSWORD') {
return new PasswordOptionModel(attrs, options);
} else {
return new OptionModel(attrs, options);
}
},
initialize: function () {
this.bind('change:selected', this._deselectLastSelected, this);
},
selectedOption: function () {
return this.find(option => option.get('selected'));
},
passwordOption: function () {
return this.find(option => option.get('privacy') === 'PASSWORD');
},
_deselectLastSelected: function (m, isSelected) {
if (isSelected) {
this.each(function (option) {
if (option !== m) {
option.set({selected: false}, {silent: true});
}
});
}
}
}, { // Class properties:
/**
* Get a privacy options collection from a Vis model
*
* Note that since the user's permissions should change very seldom, it's reasonable to assume they will be static for
* the collection's lifecycle, so set them on the models attrs when creating the collection.
* collection is created.
*
* @param vis {Object} instance of cdb.admin.Visualization
* @param user {Object} instance of cdb.admin.User
* @returns {Object} instance of this collection
*/
byVisAndUser: function (vis, user) {
const canSelectPremiumOptions = user.get('actions')[ vis.isVisualization() ? 'private_maps' : 'private_tables' ];
const currentPrivacy = vis.get('privacy');
const availableOptions = vis.privacyOptions();
return new this(
_.chain(ALL_OPTIONS)
.filter(function (option) {
return _.contains(availableOptions, option.privacy);
})
.map(function (option) {
// Set state that depends on vis and user attrs, they should not vary during the lifecycle of this collection
return _.defaults({
selected: option.privacy === currentPrivacy,
disabled: !(option.alwaysEnable || canSelectPremiumOptions)
}, option);
})
.value()
);
}
});
@@ -0,0 +1,37 @@
const _ = require('underscore');
const OptionModel = require('./option-model');
/**
* View model for the special privacy option representing a password protected map.
* It handles the logic related to the password that needs to be set for the option.
*/
const PasswordOptionModel = OptionModel.extend({
initialize: function () {
OptionModel.prototype.initialize.apply(this, arguments);
// Initially a default fake password is set, but if option is selected (like switching option) it's reset
this.set('password', PasswordOptionModel.DEFAULT_FAKE_PASSWORD);
},
/**
* @override OptionModel.attrsToSave
*/
_attrsToSave: function () {
const attrs = OptionModel.prototype._attrsToSave.call(this);
if (attrs.password === PasswordOptionModel.DEFAULT_FAKE_PASSWORD) {
delete attrs.password;
}
return attrs;
},
canSave: function () {
return OptionModel.prototype.canSave.call(this) && !_.isEmpty(this.get('password'));
}
}, {
DEFAULT_FAKE_PASSWORD: '!@#!@#'
});
module.exports = PasswordOptionModel;
@@ -0,0 +1,104 @@
const _ = require('underscore');
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const pluralizeStr = require('dashboard/helpers/pluralize');
const template = require('./start-view.tpl');
const DISABLED_SAVE_CLASS_NAME = 'is-disabled';
const SHARED_ENTITIES_SAMPLE_SIZE = 5;
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel',
'privacyOptions',
'userModel',
'visModel'
];
/**
* View represent the start screen when opening the privacy dialog.
* Display privacy options and possibly a upgrade or share banner depending on user privileges.
*/
module.exports = CoreView.extend({
events: {
'click .js-option': '_onClickOption',
'click .js-share': '_onClickShare',
'keyup .js-password-input': '_onKeyUpPasswordInput'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
// Password might not be available (i.e. for changing privacy of a dataset)
const pwdOption = this._privacyOptions.passwordOption();
const password = pwdOption ? pwdOption.get('password') : '';
const selectedOption = this._privacyOptions.selectedOption();
const upgradeUrl = this._configModel.get('upgrade_url');
const sharedEntities = this._visModel.permission.getUsersWithAnyPermission();
this.$el.html(
template({
vis: this._visModel,
privacyOptions: this._privacyOptions,
password,
saveBtnClassNames: selectedOption.canSave() ? '' : DISABLED_SAVE_CLASS_NAME,
showUpgradeBanner: upgradeUrl && this._privacyOptions.any(o => !!o.get('disabled')),
upgradeUrl,
showTrial: this._userModel.canStartTrial(),
showShareBanner: this._userModel.organization,
sharedEntitiesCount: sharedEntities.length,
personOrPeopleStr: pluralizeStr('person', 'people', sharedEntities.length),
sharedEntitiesSampleCount: SHARED_ENTITIES_SAMPLE_SIZE,
sharedEntitiesSample: _.take(sharedEntities, SHARED_ENTITIES_SAMPLE_SIZE),
sharedWithOrganization: this._visModel.permission.isSharedWithOrganization()
})
);
this.delegateEvents();
return this;
},
_initBinds: function () {
this.listenTo(this._privacyOptions, 'change:selected change:disabled', this.render);
this.listenTo(this._privacyOptions, 'change:password', this._onChangePassword);
},
_onClickOption: function (event) {
const index = $(event.target).closest('.js-option').data('index');
const option = this._privacyOptions.at(index);
if (!option.get('disabled')) {
option.set('selected', true);
}
const pwdOption = this._privacyOptions.passwordOption();
if ((option === pwdOption) && (!option.get('disabled'))) {
this.$('.js-password-input')
.val('') // reset any existing input value
.focus()
.keyup(); // manually trigger a key up event to change password state
} else if (pwdOption) { // Password might not be available (i.e. for changing privacy of a dataset)
this.$('.js-password-input').val(pwdOption.get('password'));
}
},
_onChangePassword: function () {
this.$('.ok').toggleClass(DISABLED_SAVE_CLASS_NAME, !this._privacyOptions.selectedOption().canSave());
},
_onKeyUpPasswordInput: function (event) {
this._privacyOptions.passwordOption().set('password', event.target.value);
},
_onClickShare: function (event) {
this.killEvent(event);
this.trigger('clickedShare');
}
});
@@ -0,0 +1,102 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-unlock"></i>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace--m u-tSpace-xl"><%- vis.get('name') %> 隐私</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
尽管我们相信开放数据的力量,但您也可以保护 <%- vis.isVisualization() ? '地图' : '数据集' %>.
</p>
</div>
<div class="CDB-Text Dialog-body u-inner OptionCards">
<% privacyOptions.each(function(m, index) { %>
<div class="OptionCard OptionCard--blocky <%- m.classNames() %> js-option" data-index="<%- index %>">
<div class="OptionCard-icon IllustrationIcon IllustrationIcon--<%- m.get('illustrationType') %>">
<i class="CDB-IconFont CDB-IconFont-<%- m.get('iconFontType') %>"></i>
</div>
<h5 class="OptionCard-title OptionCard-title CDB-Text CDB-Size-large"><%- m.get('title') %></h5>
<% if (m.get('privacy') == 'PASSWORD') { %>
<% if (m.get('disabled')) { %>
<input class="js-password-input Input CDB-Text CDB-Size-medium ChangePrivacy-passwordInput u-altTextColor" placeholder="Type your password here" value="<%- password %>" type="password" disabled/>
<% } else { %>
<input class="js-password-input Input CDB-Text CDB-Size-medium ChangePrivacy-passwordInput u-altTextColor" placeholder="Type your password here" value="<%- password %>" type="password" />
<% } %>
<% } else { %>
<div class="OptionCard-desc CDB-Text CDB-Size-medium u-altTextColor"><%- m.get('desc') %></div>
<% } %>
</div>
<% }); %>
</div>
<% if (showUpgradeBanner) { %>
<div class="CDB-Text Dialog-body u-inner ChangePrivacy-upgradeBanner">
<div class="UpgradeElement ChangePrivacy-upgradeBannerInner">
<div class="UpgradeElement-info">
<p class="UpgradeElement-infoText u-ellipsLongText">To get advantage of all the privacy options you should upgrade your plan</p>
</div>
<div class="UpgradeElement-actions">
<% if (showTrial) { %>
<div class="UpgradeElement-trial">
<i class="CDB-IconFont CDB-IconFont-gift UpgradeElement-trialIcon"></i>
<p class="UpgradeElement-trialText u-ellipsLongText">14 days Free trial</p>
</div>
<% } %>
<a href="<%- upgradeUrl %>" class="Button Button--secondary UpgradeElement-button ChangePrivacy-upgradeActionButton">
<span>upgrade</span>
</a>
</div>
</div>
</div>
<% } %>
<% if (showShareBanner) { %>
<% if (sharedEntitiesCount > 0) { %>
<div class="CDB-Text Dialog-body u-inner ChangePrivacy-shareBanner Dialog-affectedEntities">
<div class="Dialog-affectedEntities">
<div class="LayoutIcon ChangePrivacy-shareBannerIcon">
<i class="CDB-IconFont CDB-IconFont-people CDB-IconFont--super"></i>
<span class="Badge Dialog-headerIconBadge"><%- sharedEntitiesCount %></span>
</div>
<div class="DefaultParagraph DefaultParagraph--secondary">
<% if (sharedWithOrganization) { %>
Shared with your whole organization.
<% } else { %>
Shared with <%- sharedEntitiesCount %> <%- personOrPeopleStr %>.
<% } %>
<a href="#" class="js-share">Open sharing settings</a>
</div>
</div>
<div class="u-flex">
<% sharedEntitiesSample.forEach(function(user) { %>
<span class="UserAvatar Dialog-sharedEntitiesAvatar u-lSpace--xl">
<% if (user.get('avatar_url')) { %>
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- user.get('avatar_url') %>" alt="<%- user.get('name') || user.get('username') %>" title="<%- user.get('name') || user.get('username') %>" />
<% } else { %>
<div class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--no-src" title="<%- user.get('name') || user.get('username') %>"></div>
<% } %>
</span>
<% }); %>
<% if (sharedEntitiesCount > sharedEntitiesSampleCount) { %>
<div class="UserAvatar Dialog-sharedEntitiesAvatar">
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar--moreItems" />
</div>
<% } %>
</div>
</div>
<% } else { %>
<div class="Dialog-body u-inner ChangePrivacy-shareBanner">
<div class="LayoutIcon ChangePrivacy-shareBannerIcon">
<i class="CDB-IconFont CDB-IconFont-people CDB-IconFont--super"></i>
</div>
<div class="DefaultParagraph DefaultParagraph--secondary CDB-Text CDB-Size-medium">Team work is always better. <a href="#" class="js-share">Share it with your colleagues</a></div>
</div>
<% } %>
<% } %>
<div class="CDB-Text Dialog-footer Dialog-footer--simple u-inner ChangePrivacy-startFooter">
<button class="CDB-Button CDB-Button--secondary cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">取消</span>
</button>
<button class="ok u-lSpace--xl CDB-Button CDB-Button--primary <%- saveBtnClassNames %>">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">保存</span>
</button>
</div>
@@ -0,0 +1,275 @@
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const template = require('./dialog-view.tpl');
const FooterView = require('./footer/create-footer-view');
const NavigationView = require('builder/components/modals/add-layer/content/navigation-view');
const ListingView = require('builder/components/modals/add-layer/content/listing-view');
const TabPaneView = require('builder/components/tab-pane/tab-pane-view');
const TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
const ViewFactory = require('builder/components/view-factory');
const renderLoading = require('builder/components/loading/render-loading');
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
const CreateMapModel = require('dashboard/views/dashboard/create-map-model');
const CreateDatasetModel = require('dashboard/views/dashboard/create-dataset-model');
const VisualizationModel = require('dashboard/data/visualization-model');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const DEFAULT_VIS_NAME = 'Untitled map';
const REQUIRED_OPTS = [
'modalModel',
'createModel',
'configModel',
'userModel',
'pollingModel',
'routerModel',
'modalModel',
'mamufasView'
];
/**
* Create map/dataset dialog, typically used from editor
*/
const CreateDialogView = CoreView.extend({
className: 'Dialog-content Dialog-content--expanded',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
this._initBinds();
// Stop pollings and prevent import modal to appear
this._mamufasView.disable();
this._pollingModel.stopPollings();
},
render: function () {
this.clearSubViews();
this.$el.html(template());
this._initViews();
this._createModel.viewsReady();
},
_initModels: function () {
this._guessingModel = new Backbone.Model({
guessing: true
});
this._privacyModel = new Backbone.Model({
privacy: this._userModel.canCreatePrivateDatasets() ? 'PRIVATE' : 'PUBLIC'
});
},
_initBinds: function () {
this.listenTo(this._createModel, 'change:contentPane', this._onChangeContentView);
this.listenTo(this._createModel, 'destroyModal', () => this._modalModel.destroy());
this.listenTo(this._pollingModel, 'importByUploadData', this._modalModel.destroy.bind(this._modalModel));
},
_initViews: function () {
this._navigationView = new NavigationView({
el: this.$('.js-navigation'),
userModel: this._userModel,
routerModel: this._createModel.getVisualizationFetchModel(),
createModel: this._createModel,
tablesCollection: this._createModel.getTablesCollection(),
configModel: this._configModel
});
this._navigationView.render();
this.addView(this._navigationView);
this._tabPaneCollection = new TabPaneCollection([
{
name: 'listing',
selected: this._createModel.get('contentPane') === 'listing',
createContentView: () => {
return new ListingView({
createModel: this._createModel,
configModel: this._configModel,
userModel: this._userModel,
privacyModel: this._privacyModel,
guessingModel: this._guessingModel
});
}
}, {
name: 'creatingFromScratch',
selected: this._createModel.get('contentPane') === 'creatingFromScratch',
createContentView: () => {
return ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.add-layer.create-loading-title')
})
);
}
}, {
name: 'loading',
selected: this._createModel.get('contentPane') === 'loading',
createContentView: () => {
return ViewFactory.createByHTML(
renderLoading({
title: _t('components.modals.create-dialog.creating-map')
})
);
}
}, {
name: 'importFailed',
selected: this._createModel.get('contentPane') === 'importFailed',
createContentView: () => {
const currentImport = this._createModel.get('currentImport');
this._createModel.set('currentImport', null);
return new ErrorDetailsView({
error: currentImport && currentImport.getError(),
userModel: this._userModel,
configModel: this._configModel
});
}
}, {
name: 'datasetQuotaExceeded',
selected: this._createModel.get('contentPane') === 'datasetQuotaExceeded',
createContentView: () => {
return new ErrorDetailsView({
error: { errorCode: 8002 },
userModel: this._userModel,
configModel: this._configModel
});
}
}
]);
var tabPaneView = new TabPaneView({
collection: this._tabPaneCollection
});
this.addView(tabPaneView);
this.$('.js-content-container').append(tabPaneView.render().el);
this._footerView = new FooterView({
configModel: this._configModel,
createModel: this._createModel,
userModel: this._userModel,
privacyModel: this._privacyModel,
guessingModel: this._guessingModel
});
this._footerView.on('destroyModal', () => this._modalModel.destroy());
this.addView(this._footerView);
this.$('.js-footer').append(this._footerView.render().el);
},
_onChangeContentView: function () {
var context = this._createModel.get('contentPane');
var paneModel = _.first(this._tabPaneCollection.where({ name: context }));
var paneModelName = paneModel.get('name');
paneModel.set('selected', true);
if (paneModelName === 'loading' || paneModelName === 'creatingFromScratch' || paneModelName === 'importFailed') {
var hiddenStyle = {
visibility: 'hidden',
opacity: '0'
};
this._footerView.$el.css(hiddenStyle);
this._navigationView.hide();
}
if (paneModelName !== 'listing') {
this._navigationView.hide();
}
},
clean: function () {
this._mamufasView.enable();
this._pollingModel.startPollings();
CoreView.prototype.clean.apply(this, arguments);
}
}, {
setViewProperties: function (opts) {
const mapModel = new CreateMapModel({}, {
userModel: opts.userModel,
configModel: opts.configModel,
backgroundPollingModel: opts.pollingModel,
backgroundPollingView: opts.pollingView
});
_.extend(this, {
configModel: opts.configModel,
userModel: opts.userModel,
pollingModel: opts.pollingModel,
pollingView: opts.pollingView,
routerModel: opts.routerModel,
mapModel
});
},
addProperties: function (properties) {
_.extend(this, properties);
},
openDialog: function (dialogDependencies, dialogOpts) {
let createModel;
if (dialogOpts.type === 'dataset') {
createModel = new CreateDatasetModel({}, {
userModel: this.userModel,
configModel: this.configModel,
backgroundPollingView: this.pollingView
});
} else {
this.mapModel.set({
listing: 'datasets',
collectionFetched: false
});
this.mapModel.setSelected(dialogOpts.selectedItems);
createModel = this.mapModel;
}
const dialogView = new CreateDialogView({
modalModel: dialogDependencies.modalModel,
configModel: this.configModel,
createModel,
userModel: this.userModel,
pollingModel: this.pollingModel,
routerModel: this.routerModel,
mamufasView: this.mamufasView,
el: dialogOpts.viewElement
});
createModel.bind('datasetError', function (resp) {
if (resp.responseText.indexOf('You have reached your table quota') !== -1) {
createModel.set('contentPane', 'datasetQuotaExceeded');
}
});
createModel.bind('datasetCreated', tableMetadata => {
let vis;
if (this.routerModel.model.isDatasets()) {
vis = new VisualizationModel({ type: 'table' }, { configModel: this.configModel });
vis.permission.owner = this.userModel;
vis.set('table', tableMetadata.toJSON());
window.location = vis.viewUrl(this.userModel).edit();
} else {
vis = new VisualizationModel({ name: DEFAULT_VIS_NAME }, { configModel: this.configModel });
vis.permission.owner = this.userModel;
vis.save({
tables: [ tableMetadata.get('id') ]
}, {
success: m => {
window.location = vis.viewUrl(this.userModel).edit();
},
error: function (e) {
dialogDependencies.modalModel.destroy();
}
});
}
});
return dialogView;
}
});
module.exports = CreateDialogView;
@@ -0,0 +1,10 @@
<div class="Dialog-header Dialog-header--expanded CreateDialog-header with-separator">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-add"></i>
</div>
<h2 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace">新建数据集</h2>
<h3 class="CDB-Text CDB-Size-medium u-altTextColor">选择合适的数据集或新建</h3>
</div>
<div class="Filters Filters--navListing Filters--static js-navigation"></div>
<div class="js-content-container Dialog-body Dialog-body--expanded Dialog-body--create Dialog-body--noPaddingTop Dialog-body--withoutBorder"></div>
<div class="Dialog-footer Dialog-footer--expanded CreateDialog-footer js-footer"></div>
@@ -0,0 +1,131 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const GuessingTogglerView = require('builder/components/modals/add-layer/footer/guessing-toggler-view');
const PrivacyTogglerView = require('builder/components/modals/add-layer/footer/privacy-toggler-view');
const template = require('./dialog-footer.tpl');
const GAPusher = require('dashboard/common/analytics-pusher');
/**
* Create footer view
*
* It will show possible choices depending the
* selected option and the state of the main model
*
*/
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel',
'userModel',
'createModel'
];
module.exports = CoreView.extend({
events: {
'click .js-templates': '_goToTemplates',
'click .js-create_map': '_createMap',
'click .js-connect': '_connectDataset'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._guessingModel = new Backbone.Model({ guessing: true });
this._privacyModel = new Backbone.Model({
privacy: this._userModel.canCreatePrivateDatasets() ? 'PRIVATE' : 'PUBLIC'
});
this._initBinds();
},
render: function () {
this.clearSubViews();
const userCanUpgrade = window.upgrade_url && !this._configModel.get('cartodb_com_hosted') && (!this._userModel.isInsideOrg() || this._userModel.isOrgOwner());
this.$el.html(
template({
isMapType: this._createModel.isMapType(),
option: this._createModel.getOption(),
listingState: this._createModel.get('listing'),
isLibrary: this._createModel.visFetchModel.get('library'),
importState: this._createModel.getImportState(),
isUploadValid: this._createModel.upload.isValidToUpload(),
selectedDatasetsCount: this._createModel.selectedDatasets.length,
maxSelectedDatasets: this._userModel.getMaxLayers(),
mapTemplate: this._createModel.get('mapTemplate'),
userCanUpgrade: userCanUpgrade,
upgradeUrl: window.upgrade_url,
currentUrl: window.location.href
})
);
this._initViews();
return this;
},
_initBinds: function () {
this.listenTo(this._createModel, 'change:upload', this.render);
this.listenTo(this._createModel, 'change:option', this.render);
this.listenTo(this._createModel, 'change:listing', this.render);
this.listenTo(this._createModel.selectedDatasets, 'all', this.render);
this.listenTo(this._createModel.visFetchModel, 'change:library', this.render);
},
_initViews: function () {
this.guessingTogglerView = new GuessingTogglerView({
guessingModel: this._guessingModel,
createModel: this._createModel,
configModel: this._configModel,
privacyModel: this._privacyModel,
userModel: this._userModel
});
this.$('.js-footer-info').append(this.guessingTogglerView.render().el);
this.addView(this.guessingTogglerView);
this.privacyTogglerView = new PrivacyTogglerView({
privacyModel: this._privacyModel,
userModel: this._userModel,
createModel: this._createModel,
configModel: this._configModel
});
this.$('.js-footerActions').prepend(this.privacyTogglerView.render().el);
this.addView(this.privacyTogglerView);
},
_connectDataset: function () {
if (this._createModel.upload.isValidToUpload()) {
// Setting privacy for new import if toggler is enabled
if (this._createModel.showPrivacyToggler()) {
this._createModel.upload.set('privacy', this._privacyModel.get('privacy'));
}
// Set proper guessing values before starting the upload
this._createModel.upload.setGuessing(this._guessingModel.get('guessing'));
this._createModel.startUpload();
this.trigger('destroyModal');
}
},
_goToTemplates: function (e) {
if (e) e.preventDefault();
this._createModel.set('option', 'templates');
},
_createMap: function () {
GAPusher({
eventName: 'send',
hitType: 'event',
eventCategory: 'Create Map',
eventAction: 'click',
eventLabel: 'Add dataset modal'
});
const selectedDatasets = this._createModel.getSelectedDatasetsCollection();
if (selectedDatasets.length > 0 && selectedDatasets.length <= this._userModel.getMaxLayers()) {
this._createModel.createMap();
}
}
});
@@ -0,0 +1,50 @@
<% if (option !== "loading") { %>
<div class="CreateDialog-footerShadow"></div>
<div class="CreateDialog-footerLine"></div>
<div class="CreateDialog-footerInner u-flex u-alignCenter u-justifySpace">
<% if (option === 'listing') { %>
<% if (listingState === "datasets") { %>
<% if (isLibrary && !isMapType) { %>
<div class="CDB-Text CDB-Size-medium u-altTextColor u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-info CreateDialog-footerInfoIcon HighlightIcon HighlightIcon--warning"></i> Once you click over one of these items it will be imported to your account.
</div>
<% } else { %>
<div class="CDB-Text CDB-Size-medium u-altTextColor u-flex u-alignCenter">
<% if (selectedDatasetsCount < maxSelectedDatasets) { %>
<%- selectedDatasetsCount %> dataset<%- selectedDatasetsCount !== 1 ? 's' : '' %>
<% } else { %>
You have reached the max layers for a new map (<%- maxSelectedDatasets %> max)
<% } %>
</div>
<div class="CreateDialog-footerActions">
<% if (selectedDatasetsCount === maxSelectedDatasets && userCanUpgrade) { %>
<a class="Button Button--main CreateDialog-footerActionsButton is-separated js-upgrade" href="<%- upgradeUrl %>"><span></span></a>
<% } %>
<button class="CDB-Button CDB-Button--primary CreateDialog-footerActionsButton js-create_map track-onboarding--createMap <%- selectedDatasetsCount === 0 ? 'is-disabled' : '' %>">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"></span>
</button>
</div>
<% } %>
<% } %>
<% if (listingState === "import") { %>
<% if (importState === 'scratch') { %>
<% if (isMapType) { %>
<div class="CreateDialog-footerInfo">
<i class="CDB-IconFont CDB-IconFont-info CreateDialog-footerInfoIcon HighlightIcon HighlightIcon--warning"></i>New on CARTO? Start with one of <a href="#/templates" class="js-templates">our templates</a>.
</div>
<% } %>
<% } else { %>
<div class="js-footer-info CreateDialog-footerInfo"></div>
<div class="CreateDialog-footerActions js-footerActions">
<button class="CDB-Button CDB-Button--primary CreateDialog-footerActionsButton <% if (!isUploadValid) { %>is-disabled<% } %> js-connect">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase"></span>
</button>
</div>
<% } %>
<% } %>
<% } %>
</div>
<% } %>
@@ -0,0 +1,130 @@
const _ = require('underscore');
const Backbone = require('backbone');
const batchProcessItems = require('dashboard/helpers/batch-process-items');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'contentType'
];
/**
* View model for delete items view.
* Manages the states changes for the delete items view.
*/
module.exports = Backbone.Collection.extend({
initialize: function (models, options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
state: function () {
return this._state;
},
errorMessage: function () {
return this._errorMessage;
},
setState: function (newState) {
this._state = newState;
this.trigger('change');
this.trigger(newState);
},
isDeletingDatasets: function () {
return this._contentType === 'datasets';
},
loadPrerequisites: function () {
const setStateToConfirmDeletion = this.setState.bind(this, 'ConfirmDeletion');
if (this.isDeletingDatasets()) {
this.setState('LoadingPrerequisites');
batchProcessItems({
howManyInParallel: 5,
items: this.toArray(),
processItem: this._loadPrerequisitesForModel,
done: setStateToConfirmDeletion,
fail: this.setState.bind(this, 'LoadPrerequisitesFail')
});
} else {
setStateToConfirmDeletion();
}
},
affectedEntities: function () {
return this.chain()
.map(function (m) {
return m.sharedWithEntities();
})
.flatten().compact().value();
},
affectedVisData: function () {
const visData = this.chain()
.map(function (m) {
const metadata = m.tableMetadata();
return []
.concat(metadata.get('dependent_visualizations'))
.concat(metadata.get('non_dependent_visualizations'));
})
.flatten().compact().value();
return _.uniq(visData, function (metadata) {
return metadata.id;
});
},
deleteItems: function () {
this.setState('DeletingItems');
// INFO: Don't put more than 1 delete in parallel because this lead to a
// race condition in the derived map deletion (if any)
batchProcessItems({
howManyInParallel: 1,
items: this.toArray(),
processItem: this._deleteItem,
done: this.setState.bind(this, 'DeleteItemsDone'),
fail: this._deletionFailed.bind(this)
});
},
_deletionFailed: function (error) {
this._errorMessage = error;
this.setState('DeleteItemsFail');
},
_loadPrerequisitesForModel: function (m, callback) {
const metadata = m.tableMetadata();
// TODO: extract to be included in fetch call instead? modifying global state is not very nice
metadata.no_data_fetch = true;
metadata.fetch({
wait: true, // TODO: from old code (delete_dialog), why is it necessary?
success: function () {
callback();
},
error: function (model, jqXHR) {
callback(jqXHR.responseText);
}
});
},
_deleteItem: function (item, callback) {
item.destroy({ wait: true })
.done(callback)
.fail(function (response) {
let errorMessage;
try {
errorMessage = JSON.parse(response.responseText).errors.join('; ');
} catch (e) {
errorMessage = 'something failed';
}
callback(errorMessage);
});
}
});
@@ -0,0 +1,173 @@
const $ = require('jquery');
const moment = require('moment');
const CoreView = require('backbone/core-view');
const pluralizeString = require('dashboard/helpers/pluralize');
const loadingView = require('builder/components/loading/render-loading');
const failTemplate = require('dashboard/components/fail.tpl');
const template = require('./delete-items.tpl');
const VisualizationModel = require('dashboard/data/visualization-model');
const MapCardPreview = require('dashboard/components/mapcard-preview-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const AFFECTED_ENTITIES_SAMPLE_COUNT = 3;
const REQUIRED_OPTS = [
'viewModel',
'userModel',
'configModel',
'modalModel'
];
/**
* Delete items dialog
*/
module.exports = CoreView.extend({
events: {
'click .ok': 'ok',
'click .cancel': 'close'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._viewModel.loadPrerequisites();
this.listenTo(this._viewModel, 'change', function () {
if (this._viewModel.state() === 'DeleteItemsDone') {
this.close();
} else {
this.render();
}
});
this.add_related_model(this._viewModel);
},
render: function () {
this.$el.html(this.render_content());
this._loadMapPreviews();
return this;
},
/**
* @implements cdb.ui.common.Dialog.prototype.render_content
*/
render_content: function () {
return this['_render' + this._viewModel.state()]();
},
_renderLoadingPrerequisites: function () {
return loadingView({
title: `Checking what consequences deleting the selected ${this._pluralizedContentType()} would have...`
});
},
_renderLoadPrerequisitesFail: function () {
return failTemplate({
msg: 'Failed to check consequences of deleting the selected ' + this._pluralizedContentType()
});
},
_renderConfirmDeletion: function () {
// An entity can be an User or Organization
const affectedEntities = this._viewModel.affectedEntities();
const affectedVisData = this._viewModel.affectedVisData();
return template({
firstItemName: this._getFirstItemName(),
selectedCount: this._viewModel.length,
isDatasets: this._viewModel.isDeletingDatasets(),
pluralizedContentType: this._pluralizedContentType(),
affectedEntitiesCount: affectedEntities.length,
affectedEntitiesSample: affectedEntities.slice(0, AFFECTED_ENTITIES_SAMPLE_COUNT),
affectedEntitiesSampleCount: AFFECTED_ENTITIES_SAMPLE_COUNT,
affectedVisCount: affectedVisData.length,
pluralizedMaps: pluralizeString('map', affectedVisData.length),
affectedVisVisibleCount: affectedVisData.length,
visibleAffectedVis: this._prepareVisibleAffectedVisForTemplate(affectedVisData)
});
},
_prepareVisibleAffectedVisForTemplate: function (visibleAffectedVisData) {
return visibleAffectedVisData.map(function (visData) {
const vis = new VisualizationModel(visData, { configModel: this._configModel });
const owner = vis.permission.owner;
return {
visId: vis.get('id'),
name: vis.get('name'),
url: vis.viewUrl(this._userModel).edit(),
owner: owner,
ownerName: owner.get('username'),
isOwner: vis.permission.isOwner(this._userModel),
showPermissionIndicator: !vis.permission.hasWriteAccess(this._userModel),
timeDiff: moment(vis.get('updated_at')).fromNow(),
authTokens: vis.get('auth_tokens').join(';')
};
}, this);
},
/**
* @overrides BaseDialog.prototype.ok
*/
ok: function () {
this._viewModel.deleteItems();
this.render();
},
close: function () {
this._modalModel.destroy();
},
_loadMapPreviews: function () {
const currentView = this;
this.$el.find('.MapCard').each(function () {
var username = $(this).data('visOwnerName');
var mapCardPreview = new MapCardPreview({
config: currentView._configModel,
el: $(this).find('.js-header'),
width: 298,
height: 130,
mapsApiResource: currentView._configModel.getMapsResourceName(username),
visId: $(this).data('visId'),
username: username,
authTokens: $(this).data('visAuthTokens').split(';')
}).load();
currentView.addView(mapCardPreview);
});
},
_renderDeletingItems: function () {
return loadingView({
title: `Deleting the selected ${this._pluralizedContentType()}...`
});
},
_renderDeleteItemsFail: function () {
let message = this._viewModel.errorMessage().replace(/\n/g, '<br>');
if (message === 'something failed') {
message = '';
}
return failTemplate({
msg: `Failed to delete the selected ${this._pluralizedContentType()}. ${message}`
});
},
_pluralizedContentType: function () {
return pluralizeString(
this._viewModel.isDeletingDatasets() ? 'dataset' : 'map',
this._viewModel.length
);
},
_getFirstItemName: function () {
if (!this.options.viewModel) return;
var firstItem = this.options.viewModel.at(0);
if (firstItem) {
return firstItem.get('name');
}
}
});
@@ -0,0 +1,89 @@
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-trash"></i>
<span class="Badge Badge--negative Dialog-headerIconBadge CDB-Text CDB-Size-small"><%- selectedCount %></span>
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">
<% if (selectedCount > 1) { %>
<%- pluralizedContentType %> <%- selectedCount %>.
<% } else { %>
<%- pluralizedContentType %> <%- firstItemName %>.
<% } %>
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
<% if (affectedVisCount > 0) { %>
<strong><%- affectedVisCount %> <%- pluralizedMaps %></strong> .
<% } %>
删除 <%- pluralizedContentType %> 将不能被恢复, 请仔细确定.
</p>
<% if (isDatasets) { %>
<p class="CDB-Text CDB-Size-medium u-altTextColor">.</p>
<% } %>
</div>
<% if (affectedVisCount > 0) { %>
<ul class="Dialog-body MapsList MapsList--centerItems u-pt--0 u-pb--20 u-border-bottom">
<% visibleAffectedVis.forEach(function(vis) { %>
<li class="MapsList-item">
<div class="MapCard" data-vis-id="<%- vis.visId %>" data-vis-owner-name="<%- vis.ownerName %>" data-vis-auth-tokens="<%- vis.authTokens %>">
<a href="<%- vis.url %>" target="_blank" class="MapCard-header MapCard-header--compact js-header">
<div class="MapCard-loader"></div>
</a>
<div class="MapCard-content MapCard-content--compact">
<div class="MapCard-contentBody">
<div class="MapCard-contentBodyRow MapCard-contentBodyRow--flex">
<h3 class="CDB-Text CDB-Size-medium u-bSpace u-ellipsis u-actionTextColor">
<a href="<%- vis.url %>" target="_blank" title="<%- vis.name %>"><%- vis.name %></a>
</h3>
<% if (vis.showPermissionIndicator) { %>
<span class="CDB-Text PermissionIndicator"></span>
<% } %>
</div>
<p class="MapCard-contentBodyTimeDiff DefaultTimeDiff CDB-Text CDB-Size-small u-altTextColor">
<%- vis.timeDiff %>
<% if (!vis.isOwner) { %>
by <span class="UserAvatar">
<img class="UserAvatar-img UserAvatar-img--smaller" src="<%- vis.owner.get('avatar_url') %>" alt="<%- vis.owner.nameOrUsername() %>" title="<%- vis.owner.nameOrUsername() %>" />
</span>
<% } %>
</p>
</div>
</div>
</div>
</li>
<% }); %>
</ul>
<% } %>
<% if (affectedEntitiesCount > 0) { %>
<div class="Dialog-body Dialog-affectedEntities">
<p class="DefaultParagraph CDB-Text CDB-Size u-altTextColor">Some users will lose access to your <%- pluralizedContentType %></p>
<div class="u-flex">
<% affectedEntitiesSample.forEach(function(user) { %>
<span class="UserAvatar is-in-list">
<% if (user.get('avatar_url')) { %>
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- user.get('avatar_url') %>" alt="<%- user.get('name') || user.get('username') %>" title="<%- user.get('name') || user.get('username') %>" />
<% } else { %>
<div class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--no-src" title="<%- user.get('name') || user.get('username') %>"></div>
<% } %>
</span>
<% }); %>
<% if (affectedEntitiesCount > affectedEntitiesSampleCount) { %>
<div class="UserAvatar is-in-list">
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar--moreItems" />
</div>
<% } %>
</div>
</div>
<% } %>
<div class="Dialog-footer Dialog-footer--simple u-inner">
<div class="Dialog-footerContent MapsList-footer">
<button class="CDB-Button CDB-Button--secondary cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">取消</span>
</button>
<button class="u-lSpace--xl CDB-Button CDB-Button--error ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">确认删除</span>
</button>
</div>
</div>
@@ -0,0 +1,83 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const loadingView = require('builder/components/loading/render-loading');
const failTemplate = require('dashboard/components/fail.tpl');
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'configModel'
];
/**
* Dialog to manage duplication process of a cdb.admin.Visualization object.
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
if (!this.model) throw new Error('model is required (cdb.admin.Visualization)');
this._duplicateMap();
},
render: function () {
this.$el.html(loadingView({
title: 'Duplicating your map'
}));
return this;
},
_renderCatchedError: function (error) {
const view = new ErrorDetailsView({
error,
userModel: this._userModel,
configModel: this._configModel
});
this.$el.html(view.render().el);
},
_renderUnknownError: function () {
this.$el.html(failTemplate({
msg: "Sorry, something went wrong, but we're not sure why."
}));
},
_duplicateMap: function (newName) {
const newMapName = this.model.get('name') + ' copy';
this.model.copy(
{ name: newMapName },
{
success: newVis => {
this._redirectTo(
newVis.viewUrl(this._userModel).edit().toString()
);
},
error: (req, resp) => {
if (resp && resp.responseText.indexOf('over account public map quota') !== -1) {
this._renderCatchedError({ errorCode: 8007 });
} else {
this._showError(req);
}
}
}
);
},
_showError: function (model) {
try {
const err = _.clone(model.attributes, model.attributes.get_error_text);
this._renderCatchedError(_.extend(err, model.attributes.get_error_text));
} catch (err) {
this._renderUnknownError();
}
},
_redirectTo: function (url) {
window.location = url;
}
});
@@ -0,0 +1,10 @@
<div class="Dialog-stickyFooter">
<div class="Dialog-footer ChangePrivacy-shareFooter u-inner">
<button class="cancel CDB-Button CDB-Button--secondary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">cancel</span>
</button>
<button class="ok CDB-Button CDB-Button--primary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Save settings</span>
</button>
</div>
</div>
@@ -0,0 +1,11 @@
<div class="CDB-Text Dialog-header Dialog-header--expanded u-inner">
<button class="Dialog-backBtn js-back u-actionTextColor">
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
</button>
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-unlock"></i>
</div>
<p class="Dialog-headerTitle u-ellipsLongText"><%- name %> privacy</p>
<p class="Dialog-headerText">Select your colleagues you want to give access in the list below</p>
</div>
@@ -0,0 +1,15 @@
const ShareViewContent = require('builder/components/modals/publish/share/share-view');
const DashboardShareViewContent = ShareViewContent.extend({
_onSave: function () {},
saveACLPermissions: function () {
var permission = this._visDefinitionModel.getPermissionModel();
permission.overwriteAcl(this._sharePermissionModel);
return permission.save()
.fail(this._searchPaginationView.showError.bind(this._searchPaginationView));
}
});
module.exports = DashboardShareViewContent;
@@ -0,0 +1,77 @@
const CoreView = require('backbone/core-view');
const DashboardShareViewContent = require('dashboard/views/dashboard/dialogs/share/share-view-content');
const shareHeaderTemplate = require('./share-header.tpl');
const shareFooterTemplate = require('./share-footer.tpl');
const ViewFactory = require('builder/components/view-factory');
const loadingView = require('builder/components/loading/render-loading');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel',
'userModel',
'visModel',
'modalModel',
'modals',
'onClose'
];
/**
* Dialog to share item with other users in organization.
*/
module.exports = CoreView.extend({
className: 'Dialog-content content is-newContent Dialog-content--expanded',
events: {
'click .js-back': 'cancel',
'click .cancel': 'cancel',
'click .ok': 'ok'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._organization = this._userModel.organization;
},
render: function () {
this.$('.content').addClass('Dialog-content--expanded');
this.dashboardShareViewContent = new DashboardShareViewContent({
className: 'Dialog-expandedSubContent',
configModel: this._configModel,
currentUserId: this._userModel.get('id'),
organization: this._userModel.organization,
visDefinitionModel: this._visModel
});
this.$el.append([
shareHeaderTemplate({
name: this._visModel.get('name')
}),
this.dashboardShareViewContent.render().el,
shareFooterTemplate()
]);
return this;
},
cancel: function () {
this._modalModel.destroy();
this._onClose();
},
// @implements cdb.ui.common.Dialog.prototype.ok
ok: function () {
const modalModel = this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(loadingView({}));
});
this.dashboardShareViewContent.saveACLPermissions()
.done(() => {
modalModel.destroy();
this._modalModel.destroy();
this._onClose();
});
}
});
@@ -0,0 +1,147 @@
const CoreView = require('backbone/core-view');
const UploadConfig = require('builder/config/upload-config');
const ErrorDetailsView = require('builder/components/background-importer/error-details-view');
const WarningsDetailsView = require('builder/components/background-importer/warnings-details-view');
const TwitterImportDetailsView = require('builder/components/background-importer/twitter-import-details-view');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./background-import-item.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'configModel',
'showSuccessDetailsButton'
];
/**
* Import item within background importer
*
*/
module.exports = CoreView.extend({
className: 'ImportItem',
tagName: 'li',
events: {
'click .js-abort': '_removeItem',
'click .js-show_error': '_showImportError',
'click .js-show_warnings': '_showImportWarnings',
'click .js-show_stats': '_showImportStats',
'click .js-close': '_removeItem'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
this._initBinds();
},
render: function () {
const upload = this.model.get('upload');
const importModel = this.model.get('import');
let templateData = {
name: '',
state: this.model.get('state'),
progress: '',
service: '',
step: this.model.get('step'),
url: '',
failed: this.model.hasFailed(),
completed: this.model.hasCompleted(),
warnings: this.model.getWarnings(),
showSuccessDetailsButton: this._showSuccessDetailsButton,
tables_created_count: importModel.tables_created_count
};
// URL
if (templateData.state === 'complete') {
const vis = this.model.importedVis();
if (vis) {
templateData.url = encodeURI(vis.viewUrl(this._userModel).edit());
}
}
// Name
if (upload.type) {
if (upload.type === 'file') {
if (upload.value.length > 1) {
templateData.name = upload.value.length + ' files';
} else {
templateData.name = upload.value.name;
}
}
if (upload.type === 'url' || upload.type === 'remote') {
templateData.name = upload.value;
}
if (upload.type === 'service') {
templateData.name = upload.value && upload.value.filename || '';
}
if (upload.service_name === 'twitter_search') {
templateData.name = 'Twitter import';
}
if (upload.type === 'sql') {
templateData.name = 'SQL';
}
if (upload.type === 'duplication') {
templateData.name = upload.table_name || upload.value;
}
} else {
templateData.name = importModel.display_name || importModel.item_queue_id || 'import';
}
// Service
templateData.service = upload.service_name;
// Progress
if (this.model.get('step') === 'upload') {
templateData.progress = this.model.get('upload').progress;
} else {
templateData.progress = (UploadConfig.uploadStates.indexOf(templateData.state) / UploadConfig.uploadStates.length) * 100;
}
this.$el.html(template(templateData));
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'remove', this.clean);
},
_removeItem: function () {
this.trigger('remove', this.model, this);
this.model.pause();
this.clean();
},
_showImportStats: function () {
this._modals.create(modalModel => {
return new TwitterImportDetailsView({
userModel: this._userModel,
model: this.model,
modalModel: this._modalModel
});
});
},
_showImportError: function () {
this._modals.create(() => {
return new ErrorDetailsView({
error: this.model.getError(),
userModel: this._userModel,
configModel: this._configModel
});
});
},
_showImportWarnings: function () {
this._modals.create(modalModel => {
return new WarningsDetailsView({
warnings: this.model.getWarnings(),
userModel: this._userModel
});
});
}
});
@@ -0,0 +1,46 @@
<% if (failed) { %>
<div class="ImportItem-text is-failed" title="<%- name %>">
Ouch! Error connecting <%- name %> <% if (service) { %> from <%- service %> <% } %>
</div>
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_error"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
<button class="CDB-Shape js-close">
<div class="CDB-Shape-close is-blue is-large"></div>
</button>
<% } else if (completed && !warnings) { %>
<div class="ImportItem-text is-completed" title="<%- name %>">
<%- name %> <% if (service && service != "twitter_search") { %> from <%- service %> <% } %> completed!
</div>
<% if (showSuccessDetailsButton) { %>
<% if (service && service === "twitter_search") { %>
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_stats"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
<% } else if (tables_created_count === 1) { %>
<a href="<%- url %>" class="CDB-Button CDB-Button--secondary CDB-Button--small js-show"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></a>
<% } %>
<% } %>
<button class="CDB-Shape js-close">
<div class="CDB-Shape-close is-blue is-large"></div>
</button>
<% } else if (completed && warnings) { %>
<div class="ImportItem-text has-warnings" title="<%- name %>">
Some warnings were produced for <%- name %> <% if (service) { %> from <%- service %> <% } %>
</div>
<button class="CDB-Button CDB-Button--secondary CDB-Button--small js-show_warnings"><span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small">SHOW</span></button>
<button class="CDB-Shape js-close">
<div class="CDB-Shape-close is-blue is-large"></div>
</button>
<% } else { %>
<div class="ImportItem-text" title="<%- name %>">
<span class="ImportItem-textState"><%- state %></span> <%- name %> <% if (service && service != "twitter_search") { %> from <%- service %> <% } %>
</div>
<div class="ImportItem-progress">
<div class="progress-bar">
<span class="bar-2" style="width:<%- progress %>%"></span>
</div>
</div>
<% if (state === "uploading" && step === "upload") { %>
<button class="ImportItem-closeButton js-abort">
<i class="CDB-IconFont CDB-IconFont-close ImportItem-closeButtonIcon"></i>
</button>
<% } %>
<% } %>
@@ -0,0 +1,41 @@
const CoreView = require('backbone/core-view');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'collection',
'model',
'template'
];
/*
* Content result default view
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
this.$el.html(this._template({
defaultUrl: '',
page: this._collection.options.get('page'),
isSearching: this._model.get('is_searching'),
tag: this._collection.options.get('tags'),
q: this._collection.options.get('q'),
quote: randomQuote(),
type: this._collection.options.get('type'),
totalItems: this._collection.size(),
totalEntries: this._collection.total_entries,
msg: ''
}));
return this;
},
_initBinds: function () {
this.listenTo(this._collection, 'change remove add reset', this.render);
}
});
@@ -0,0 +1,10 @@
<div class="IntermediateInfo">
<div class="LayoutIcon LayoutIcon--negative">
<i class="CDB-IconFont CDB-IconFont-cockroach"></i>
</div>
<% if (msg) { %>
<p class="CDB-Text CDB-Size-medium u-altTextColor u-tSpace-xl"><%= msg %></p>
<% } %>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">Oouch! There has been an error</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">If the problem persists contact us at <a class="js-mail-link" href="mailto:support@carto.com">support@carto.com</a>.</p>
</div>
@@ -0,0 +1,80 @@
const _ = require('underscore');
const moment = require('moment');
const CoreView = require('backbone/core-view');
const Utils = require('builder/helpers/utils');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const MapCardPreview = require('dashboard/components/mapcard-preview-view');
const template = require('./dataset-item.tpl');
const GEOM_TYPES = ['point', 'polygon', 'line', 'raster'];
const REQUIRED_OPTS = [
'configModel'
];
/**
* View representing an item in the list under datasets route.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'MapsList-item',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
const vis = this.model;
const date = vis.get('order') === 'updated_at' ? vis.get('updated_at') : vis.get('created_at');
this.$el.html(template({
vis: vis.attributes,
datasetSize: this._getDatasetSize(vis.get('table')['size']),
geomType: this._getGeometryType(vis.get('table')['geometry_types']),
account_host: this._configModel.get('account_host'),
dataset_base_url: this._configModel.get('dataset_base_url'),
dateFromNow: moment(date).fromNow()
}));
this._renderMapThumbnail();
return this;
},
_renderMapThumbnail: function () {
const username = this.model.get('permission')['owner']['username'];
const mapCardPreview = new MapCardPreview({
el: this.$('.js-header'),
username: username,
width: 298,
height: 220,
visId: this.model.get('id'),
mapsApiResource: this._configModel.getMapsResourceName(username),
config: this._configModel
});
if (this.imageURL) {
mapCardPreview.loadURL(this.imageURL);
} else {
mapCardPreview.load();
}
mapCardPreview.bind('loaded', function (url) {
this.imageURL = url;
}, this);
this.addView(mapCardPreview);
},
_getGeometryType: function (geomTypes) {
const geomType = (geomTypes && geomTypes[0] || '').toLowerCase();
return _.find(GEOM_TYPES, type => geomType.indexOf(type) !== -1);
},
_getDatasetSize: function (size) {
return size ? Utils.readablizeBytes(size, true).split(' ') : 0;
}
});
@@ -0,0 +1,41 @@
<div class="MapCard MapCard--borderless MapCard--squared js-card" data-vis-id="<%- vis.id %>" data-username="<%- vis.permission.owner.username %>">
<a href="<%- dataset_base_url %><%- vis.name %>" target="_blank" class="MapCard-header js-header">
<div class="MapCard-loader js-loader"></div>
</a>
<div class="MapCard-content">
<div class="MapCard-contentFooter MapCard-contentFooter--with-icon">
<div class="MapCard-contentFooterIcon">
<div class="DatasetsList-itemCategory is--<%= geomType %>Dataset"></div>
</div>
<div class="MapCard-contentFooterDetails u-ellipsLongText">
<div class="MapCard-contentFooterTitle">
<h3 class="MapCard-title DefaultTitle CDB-Text is-semibold CDB-Size-large">
<a href="<%- dataset_base_url %><%- vis.name %>" target="_blank" class="DefaultTitle-link u-ellipsLongText" title="<%- vis.display_name %>">
<% if (vis.display_name) { %>
<%- vis.display_name %>
<% } else { %>
<%- vis.name %>
<% } %>
</a>
</h3>
</div>
<div class="MapCard-contentFooterIcons CDB-Size-medium u-altTextColor">
<div class="MapCard-contentFooterDetails--left MapCard-contentFooterDetails--noright">
<div class="MapCard-contentFooterTimeDiff DefaultTimeDiff">
<i class="CDB-IconFont CDB-IconFont-clock DefaultTimeDiff-icon"></i>
<%- dateFromNow %>
</div>
<% if (datasetSize && datasetSize[0] > 0) { %>
<div class="MapCard-contentFooterIcon u-hideOnMobile">
<i class="CDB-IconFont CDB-IconFont-floppy SizeIndicator-icon"></i>
<span class="MapCardIcon-counter"><%- datasetSize[0] %></span> <span class="MapCardIcon-label"><%- datasetSize[1] %></span>
</div>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,82 @@
const CoreView = require('backbone/core-view');
const _ = require('underscore');
const DatasetsItemView = require('./dataset-item-view');
const PlaceholderItem = require('./placeholder-item-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel',
'collection'
];
const MAP_CARDS_PER_ROW = 3;
/**
* View representing the list of items
*/
module.exports = CoreView.extend({
tagName: 'ul',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initBinds();
},
render: function () {
if (this._collection.options.get('page') === 1) {
this.clearSubViews();
}
this._collection.each(this._addItem, this);
let className = 'MapsList';
if (this._collection._ITEMS_PER_PAGE * this._collection.options.get('page') >= this._collection.total_entries) {
className += ' is-bottom';
}
this.$el.attr('class', className);
if (this._collection.size() > 0) {
this._fillEmptySlotsWithPlaceholderItems();
}
return this;
},
_initBinds: function () {
this.listenTo(this._collection, 'reset loaded', this.render);
},
show: function () {
this.$el.removeClass('is-hidden');
},
hide: function () {
this.$el.addClass('is-hidden');
},
_addItem: function (model) {
const itemView = new DatasetsItemView({
model,
configModel: this._configModel
});
this.addView(itemView);
this.$el.append(itemView.render().el);
},
_fillEmptySlotsWithPlaceholderItems: function () {
_.times(this._emptySlotsCount(), function () {
var view = new PlaceholderItem();
this.$el.append(view.render().el);
this.addView(view);
}, this);
},
_emptySlotsCount: function () {
return (this._collection._ITEMS_PER_PAGE - this._collection.size()) % MAP_CARDS_PER_ROW;
}
});
@@ -0,0 +1,4 @@
<div class="MapCard MapCard--squared MapCard--borderless">
<div class="MapCard-header MapCard-header--fake"></div>
<div class="MapCard-content"></div>
</div>
@@ -0,0 +1,20 @@
const CoreView = require('backbone/core-view');
const template = require('./placeholder-item-template.tpl');
/**
* Represents a map card on data library.
*/
module.exports = CoreView.extend({
className: 'MapsList-item MapsList-item--fake',
tagName: 'li',
render: function () {
this.clearSubViews();
this.$el.html(template());
return this;
}
});
@@ -0,0 +1,5 @@
<div class="IntermediateInfo">
<div class="Spinner"></div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m"><%- q || tag ? 'Searching' : 'Loading' %>...</h4>
<div class="CDB-Text CDB-Size-medium u-altTextColor"><%= quote %></div>
</div>
@@ -0,0 +1,3 @@
<div class="u-inner u-txt-center">
<button class="Button Button--gray Button--centered is-hidden js-more DataLibrary-more">View more</button>
</div>
@@ -0,0 +1,11 @@
<div class="IntermediateInfo">
<div class="LayoutIcon">
<i class="CDB-IconFont CDB-IconFont-lens" />
</div>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m">
Oh! No results
</h4>
<p class="CDB-Text CDB-Size-medium u-altTextColor">
Your search was correct but returned no results, please try with a different set of parameters before running it again
</p>
</div>
@@ -0,0 +1,11 @@
<div class="IntermediateInfo">
<div class="LayoutIcon LayoutIcon--negative">
<i class="CDB-IconFont CDB-IconFont-info"></i>
</div>
<% if (msg) { %>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl"><%= msg %></h4>
<% } else { %>
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-secondaryTextColor u-bSpace--m u-tSpace-xl">Oouch! There has been an error</h4>
<% } %>
<p class="CDB-Text CDB-Size-medium u-altTextColor">If the problem persists contact us at <a class="js-mail-link" href="mailto:support@carto.com">support@carto.com</a>.</p>
</div>
@@ -0,0 +1,271 @@
const $ = require('jquery');
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const FiltersView = require('dashboard/views/data-library/filters/filters-view');
const ListView = require('dashboard/views/data-library/content/list/list-view');
const ContentView = require('dashboard/views/data-library/content/content-view');
const DatasetsCollection = require('dashboard/data/datasets-collection');
const DataLibraryHeaderView = require('dashboard/views/data-library/header/header-view');
const moreTemplate = require('dashboard/views/data-library/content/more-template.tpl');
const noResultsTemplate = require('dashboard/views/data-library/content/no-results-template.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const loaderTemplate = require('dashboard/views/data-library/content/loader-template.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'configModel'
];
module.exports = CoreView.extend({
events: {
'click .js-more': '_onClickMore'
},
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
this._initModels();
this._initViews();
this._initBinds();
},
render: function () {
this._fetchCollection();
return this;
},
_initModels: function () {
this.model = new Backbone.Model({
vis_count: 0,
show_countries: false,
is_searching: false
});
this.collection = new DatasetsCollection(null, { configModel: this._configModel });
this._resetOptions();
},
_initViews: function () {
this.controlledViews = {}; // All available views
this.enabledViews = []; // Visible views
const dataLibraryHeader = new DataLibraryHeaderView({
model: this.model,
collection: this.collection,
configModel: this._configModel
});
$('.js-Header--datalibrary').append(dataLibraryHeader.render().el);
this.addView(dataLibraryHeader);
dataLibraryHeader.load();
const filtersView = new FiltersView({
collection: this.collection,
model: this.model
});
$('.Filters').append(filtersView.render().el);
this.addView(filtersView);
const moreView = new ContentView({
model: this.model,
collection: this.collection,
template: moreTemplate
});
this.$el.append(moreView.render().el);
this.addView(moreView);
const listView = new ListView({
collection: this.collection,
configModel: this._configModel
});
$('.js-DataLibrary-content').append(listView.render().el);
this.addView(listView);
this.controlledViews['list'] = listView;
const noResultsView = new ContentView({
model: this.model,
collection: this.collection,
template: noResultsTemplate
});
this.$el.append(noResultsView.render().el);
this.addView(noResultsView);
this.controlledViews['no_results'] = noResultsView;
const errorView = new ContentView({
model: this.model,
collection: this.collection,
template: errorTemplate
});
this.$el.append(errorView.render().el);
this.addView(errorView);
this.controlledViews['error'] = errorView;
const mainLoaderView = new ContentView({
model: this.model,
collection: this.collection,
template: loaderTemplate
});
this.$el.append(mainLoaderView.render().el);
this.addView(mainLoaderView);
this.controlledViews['main_loader'] = mainLoaderView;
},
_fetchCollection: function () {
this.collection.fetch();
},
_initBinds: function () {
this.model.bind('change:show_more', this._onChangeShowMore, this);
this.model.bind('change:vis_count', this._onChangeVisCount, this);
this.listenTo(this.collection.options, 'change:tags', this._resetVisCount);
this.listenTo(this.collection.options, 'change:bbox', this._resetVisCount);
this.listenTo(this.collection.options, 'change', this._onCollectionOptionsChange);
this.listenTo(this.collection, 'reset loaded', this._onCollectionReset);
this.listenTo(this.collection, 'loading', this._onCollectionLoading);
this.listenTo(this.collection, 'error', this._onCollectionError);
},
_resetVisCount: function () {
this.model.set({ vis_count: 0 });
},
_onCollectionOptionsChange: function () {
this.model.set({ show_more: false });
this._fetchCollection();
},
_onCollectionError: function (collection, event, opts) {
if (!event || (event && event.statusText !== 'abort')) {
this._onDataError(event);
}
},
_onCollectionLoading: function () {
if (this.collection.options.get('page') === 1) {
this._showLoader();
} else {
this._showLoaderOnly();
}
},
_onCollectionReset: function () {
this._onDataFetched();
},
_onChangeVisCount: function () {
if (this.model.get('vis_count') >= this.collection.total_entries) {
this.model.set({ show_more: false });
} else {
this.model.set({ show_more: true });
}
},
_onChangeShowMore: function () {
this.$('.js-more').toggleClass('is-hidden', !this.model.get('show_more'));
},
_onDataFetched: function () {
const activeViews = [];
if (this.collection.size() === 0) {
activeViews.push('no_results');
} else {
this.model.set({
vis_count: this.model.get('vis_count') + this.collection.length,
show_more: true
});
activeViews.push('list');
}
this._hideBlocks();
this._showBlocks(activeViews);
},
_onDataError: function (error) {
if (window.trackJs && window.trackJs.track) {
window.trackJs.track(error);
}
this._hideBlocks();
this._showBlocks(['error']);
},
_showBlocks: function (views) {
if (views) {
_.each(views, (view) => {
this.controlledViews[view].show();
this.enabledViews.push(view);
});
} else {
this.enabledViews = [];
_.each(this.controlledViews, (view) => {
view.show();
this.enabledViews.push(view);
});
}
},
_hideBlocks: function (views) {
if (views) {
_.each(views, (view) => {
this.controlledViews[view].hide();
this.enabledViews = _.without(this.enabledViews, view);
});
} else {
_.each(this.controlledViews, (view) => {
view.hide();
});
this.enabledViews = [];
}
},
_isBlockEnabled: function (name) {
if (name) {
return _.contains(this.enabledViews, name);
}
return false;
},
_showLoader: function () {
this._hideBlocks();
this._showBlocks(['main_loader']);
},
_showLoaderOnly: function () {
this._showBlocks(['main_loader']);
},
_hideLoader: function () {
this._hideBlocks(['main_loader']);
},
_resetOptions: function () {
this.collection.options.set({
q: '',
order: 'updated_at',
page: 1,
tags: '',
bbox: '',
source: [],
type: 'table'
});
},
_onClickMore: function (event) {
this.killEvent(event);
this.model.set({ show_more: false });
this.collection.options.set({
page: this.collection.options.get('page') + 1
});
}
});
@@ -0,0 +1,49 @@
const $ = require('jquery');
const DropdownAdminView = require('dashboard/components/dropdown/dropdown-admin-view');
const template = require('./dropdown.tpl');
/**
* The content of the dropdown menu opened by the user in the data-library filters, e.g.:
* Category ▼
* ______/\____
* | |
* | this |
* |____________|
*/
module.exports = DropdownAdminView.extend({
className: 'CDB-Text Dropdown Dropdown--public',
events: {
'click .js-all': '_onClickAll',
'click .js-categoryLink': '_onClickLink'
},
render: function () {
this.$el.html(template());
// TODO: taken from existing code, how should dropdowns really be added to the DOM?
$('body').append(this.el);
return this;
},
_onClickAll: function (event) {
this.collection.options.set({
tags: '',
page: 1
});
this.hide();
},
_onClickLink: function (event) {
var tag = $(event.target).text();
this.collection.options.set({
tags: tag,
page: 1
});
this.hide();
}
});
@@ -0,0 +1,11 @@
<ul class="SettingsDropdown CDB-Size-medium">
<li class="SettingsDropdown-item SettingsDropdown-item--public">
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-all">All categories</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">Administrative regions</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">Cultural datasets</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">Physical datasets</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">Historic</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">Building footprints</button></p>
<p><button class="SettingsDropdown-itemLink SettingsDropdown-itemLink--public js-categoryLink">US Census</button></p>
</li>
</ul>
@@ -0,0 +1,113 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const DropdownView = require('./dropdown/dropdown-view');
const Utils = require('builder/helpers/utils');
const template = require('./filters.tpl');
const ESC_KEY = 27;
/**
* Dashboard filters.
*
* - 'Filter by' collection.
* - 'Search' any pattern within collection.
*
*/
module.exports = CoreView.extend({
events: {
'submit .js-search-form': '_submitSearch',
'keydown .js-search-form': '_onSearchKeyDown',
'click .js-search-form': 'killEvent',
'click .js-search-link': '_onSearchClick',
'click .js-clean-search': '_onCleanSearchClick',
'click .js-categoriesDropdown': '_createDropdown'
},
initialize: function () {
this._preRender();
this._initBinds();
},
_preRender: function () {
const $uInner = $('<div>').addClass('u-inner');
const $filtersInner = $('<div>').addClass('Filters-inner');
this.$el.append($uInner.append($filtersInner));
},
render: function () {
this.$('.Filters-inner').html(
template({
tag: this.collection.options.get('tags'),
q: this.collection.options.get('q')
})
);
return this;
},
_initBinds: function () {
this.listenTo(this.collection, 'add remove change reset', this.render);
this.listenTo(this.collection.options, 'change:tags', this.render);
},
_createDropdown: function (event) {
this._setupDropdown(new DropdownView({
target: $(event.target).closest('.js-categoriesDropdown'),
tick: 'right',
collection: this.collection
}));
},
_setupDropdown: function (dropdownView) {
this.addView(dropdownView);
dropdownView.render();
dropdownView.open();
},
_onSearchClick: function (event) {
this.killEvent(event);
this.$('.js-search-input').val('');
this.$('.js-search-input').focus();
},
_onSearchKeyDown: function (event) {
if (event.code === ESC_KEY) {
this._onSearchClick(event);
}
},
// Filter actions
_onCleanSearchClick: function (event) {
this.killEvent(event);
this._cleanSearch();
},
_submitSearch: function (event) {
this.killEvent(event);
this.model.set('is_searching', true);
this.collection.options.set({
q: Utils.stripHTML(this.$('.js-search-input').val().trim(), ''),
page: 1
});
this.render();
},
_cleanSearch: function () {
this.model.set('is_searching', false);
this.collection.options.set({
q: '',
page: 1
});
this.render();
}
});
@@ -0,0 +1,33 @@
<span class="Filters-separator"></span>
<div class="Filters-row">
<div class="Filters-group <% if (q) { %>is-searching<% } %>">
<div class="Filters-typeItem Filters-typeItem--searchEnabler js-search-enabler">
<a href="#/search" class="Filters-searchLink CDB-Text CDB-Size-medium u-actionTextColor u-flex u-upperCase is-semibold js-search-link">
<div class="CDB-Shape u-rSpace">
<div class="CDB-Shape-magnify is-small is-blue"></div>
</div>
Search
</a>
</div>
<div class="Filters-typeItem Filters-typeItem--searchField js-search-field">
<form class="Filters-searchForm js-search-form" action="#">
<input class="Filters-searchInput CDB-Text CDB-Size-medium js-search-input" type="text" value="<%- q %>" placeholder="by name" />
<% if (q) { %>
<div class="CDB-Shape js-clean-search">
<div class="CDB-Shape-close is-blue is-large"></div>
</div>
<% } %>
</form>
</div>
</div>
<ul class="Filters-group">
<li class="Filters-typeItem <% if (q) { %>is-searching<% } %>">
<a class="Filters-typeLink CDB-Text CDB-Size-medium is-semibold u-upperCase js-categoriesDropdown" href="#/category">
<%- tag == '' ? 'Category' : tag %>
</a>
</li>
</ul>
</div>
@@ -0,0 +1,53 @@
const CoreView = require('backbone/core-view');
const $ = require('jquery');
const _ = require('underscore');
const L = require('leaflet');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const template = require('./header.tpl');
const REQUIRED_OPTS = [
'configModel'
];
/**
* The header map in the data-library page, where the user can filter by country, e.g.:
*/
module.exports = CoreView.extend({
className: 'DataLibrary-header',
initialize: function (opts) {
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
_.bindAll(this, '_addGeojsonData');
},
render: function () {
this.$el.html(template());
return this;
},
load: function () {
this.map = L.map(this.$('#DataLibraryMap')[0], {
zoomControl: false,
attributionControl: false
}).setView([44, -31], 3);
var sqlDomain = this._configModel.get('sql_api_template').replace('{user}', this._configModel.get('common_data_user'));
var geojsonURL = sqlDomain + '/api/v2/sql?q=' + encodeURIComponent('select * from world_borders') + '&format=geojson&filename=world_borders';
$.getJSON(geojsonURL).done(this._addGeojsonData);
},
_addGeojsonData: function (geojsonData) {
var style = {
color: '#2E3C43',
weight: 1,
opacity: 1,
fillColor: '#242D32',
fillOpacity: 1
};
this.layer = L.geoJson(geojsonData, { style: style }).addTo(this.map);
}
});
@@ -0,0 +1,26 @@
<div class="DataLibrary-gradient"></div>
<div class="Header-inner Header-title js-Header-title u-inner">
<div class="Header-innerTitle">
<h1 class="Title-small">Data library</h1>
<p class="Title Title--l Title--white u-vspace-l">Open resources to help you populate your maps</p>
</div>
</div>
<div class="Header-inner Header-footer is-hidden">
<div class="CountrySelector js-CountrySelector u-inner">
<p class="CountrySelector-text">Filter datasets by country selecting it in this map</p>
<button class="CountrySelector-button js-country"></button>
</div>
<div class="CountrySelector CountrySelector-back js-CountrySelector-back is-hidden">
<div class="CountrySelector-inner u-inner">
<p class="CountrySelector-text">Back</p>
<button class="NavButton Dialog-countryBack js-back">
<i class="CDB-IconFont CDB-IconFont-close"></i>
</button>
</div>
</div>
</div>
<div id="DataLibraryMap" class="DataLibraryMap"></div>
@@ -0,0 +1,37 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-mobile-app.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'modalModel',
'configModel',
'authenticityToken',
'needsPasswordConfirmation'
];
module.exports = CoreView.extend({
events: {
'submit .js-form': '_close',
'click .js-cancel': '_close'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
const mobileAppId = this.options.mobileApp.id;
this.$el.html(
template({
formAction: `${this._configModel.get('base_url')}/your_apps/mobile/${mobileAppId}`,
authenticityToken: this._authenticityToken,
passwordNeeded: this._needsPasswordConfirmation
})
);
},
_close: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,35 @@
<form accept-charset="UTF-8" action="<%- formAction %>" method="post" class="js-form">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-keys"></i>
</div>
<p class="Dialog-headerTitle">You are about to delete your application</p>
<p class="Dialog-headerText">Remember, once you delete it there is no going back</p>
</div>
<% if (passwordNeeded) { %>
<div class="CDB-Text Dialog-body">
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Your password</label>
</div>
<div class="Form-rowData">
<input type="password" id="deletion_password_confirmation" name="password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</div>
<% } %>
<div class="Dialog-footer u-inner">
<button type="button" class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--error js-save">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Delete this application</span>
</button>
</div>
</form>
@@ -0,0 +1,27 @@
const Backbone = require('backbone');
/**
* Header view model to handle state for dashboard header view.
*/
module.exports = Backbone.Model.extend({
breadcrumbTitle: function () {
return 'Configuration';
},
isBreadcrumbDropdownEnabled: function () {
return false;
},
isDisplayingDatasets: function () {
return false;
},
isDisplayingMaps: function () {
return false;
},
isDisplayingLockedItems: function () {
return false;
}
});
@@ -0,0 +1,45 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-organization-user.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'modalModel',
'configModel',
'passwordNeeded',
'organizationUser',
'authenticityToken'
];
module.exports = CoreView.extend({
events: {
'click .js-cancel': '_closeDialog',
'submit .js-form': '_closeDialog'
},
options: {
authenticityToken: '',
organizationUser: {}
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
CoreView.prototype.initialize.apply(this);
},
render: function () {
this.$el.html(template({
username: this._organizationUser.get('username'),
formAction: `${this._configModel.prefixUrl()}/organization/users/${this._organizationUser.get('username')}`,
authenticityToken: this._authenticityToken,
passwordNeeded: this._passwordNeeded
}));
return this;
},
_closeDialog: function () {
if (this._modalModel) {
this._modalModel.destroy();
}
}
});
@@ -0,0 +1,41 @@
<form accept-charset="UTF-8" action="<%- formAction %>" method="post" class="js-form">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
</div>
<p class="Dialog-headerTitle">You are about to delete <%- username %>'s account.</p>
<p class="Dialog-headerText">
By deleting this account all <%- username %>'s maps and datasets will be lost,
but extra credits will be reassigned to your user.
<% if (passwordNeeded) { %>
Type your password, please.
<% } %>
</p>
</div>
<% if (passwordNeeded) { %>
<div class="CDB-Text Dialog-body">
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Your password</label>
</div>
<div class="Form-rowData">
<input type="password" id="deletion_password_confirmation" name="password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</div>
<% } %>
<div class="CDB-Text Dialog-footer u-inner">
<button class="CDB-Button CDB-Button--secondary Dialog-footerBtn js-cancel" type="button">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--error js-ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Yes, delete <%- username %> account</span>
</button>
</div>
</form>
@@ -0,0 +1,40 @@
const CoreView = require('backbone/core-view');
const template = require('./delete-organization.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'userModel',
'modalModel',
'authenticityToken'
];
/**
* When an organization owner wants to delete the full organization
*
*/
module.exports = CoreView.extend({
options: {
authenticityToken: ''
},
events: {
'click .js-cancel': '_closeDialog',
'submit .js-form': '_closeDialog'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
return this.$el.html(template({
formAction: `${this._userModel.get('base_url')}/organization`,
authenticityToken: this._authenticityToken,
passwordNeeded: !!this._userModel.get('needs_password_confirmation')
}));
},
_closeDialog: function () {
this._modalModel.destroy();
}
});
@@ -0,0 +1,41 @@
<form accept-charset="UTF-8" action="<%- formAction %>" method="post" class="js-form">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="<%- authenticityToken %>" />
<input name="_method" type="hidden" value="delete" />
<div class="CDB-Text Dialog-header u-inner">
<div class="Dialog-headerIcon Dialog-headerIcon--negative">
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
</div>
<p class="Dialog-headerTitle">You are about to delete your organization.</p>
<p class="Dialog-headerText">
You will remove this organization and all its users (including this account)<br/>
and it will not be possible to recover its information (including tables and data) after this deletion.<br/>
<% if (passwordNeeded) { %>
If you want to proceed, type your password:<br/>
<% } %>
</p>
</div>
<% if (passwordNeeded) { %>
<div class="CDB-Text Dialog-body">
<div class="Form-row Form-row--centered has-label">
<div class="Form-rowLabel">
<label class="Form-label">Your password</label>
</div>
<div class="Form-rowData">
<input type="password" name="deletion_password_confirmation" class="CDB-InputText CDB-Text Form-input Form-input--long" value=""/>
</div>
</div>
</div>
<% } %>
<div class="Dialog-footer u-inner">
<button type="button" class="CDB-Button CDB-Button--secondary js-cancel">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Cancel</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--error js-ok">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Yes, delete the organization</span>
</button>
</div>
</form>
@@ -0,0 +1,160 @@
const _ = require('underscore');
const $ = require('jquery');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const template = require('./add-group-users-view.tpl');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const GroupUsersListView = require('dashboard/views/organization/groups-admin/group-users-list/group-users-list-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel',
'modalModel'
];
/**
* Dialog to add custom basemap to current map.
*/
module.exports = CoreView.extend({
events: {
'click .ok': 'ok'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
// Include current user in fetch results
this._orgUsers.excludeCurrentUser(false);
this._modals = new ModalsServiceModel();
this._initBinds();
this._initViews();
},
clean: function () {
// restore org users
this._orgUsers.restoreExcludeCurrentUser();
CoreView.prototype.clean.apply(this);
},
/**
* @override cdb.ui.common.Dialog.prototype.render
*/
render: function () {
this.$el.html(this.render_content());
this.$el.addClass('Dialog-contentWrapper');
this._onChangeSelected();
return this;
},
/**
* @implements cdb.ui.common.Dialog.prototype.render_content
*/
render_content: function () {
switch (this.model.get('state')) {
case 'saving':
return loadingView({
title: 'Adding users to group'
});
case 'passwordConfirmationFail':
return requestErrorTemplate({
msg: this.model.get('failMessage')
});
case 'saveFail':
return errorTemplate({
msg: ''
});
default:
const $content = $(template());
$content.find('.js-dlg-body').replaceWith(this._PagedSearchView.render().el);
return $content;
}
},
ok: function () {
const selectedUsers = this._selectedUsers();
if (!selectedUsers.length) return;
if (!this._userModel.needsPasswordConfirmation()) {
this.model.set('state', 'saving');
return this._addUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => {
this.model.set('state', 'saving');
this._addUsers(password);
}
});
},
_initViews: function () {
this._PagedSearchView = new PagedSearchView({
isUsedInDialog: true,
pagedSearchModel: new PagedSearchModel({
per_page: 50,
order: 'username'
}),
collection: this._orgUsers,
createListView: this._createUsersListView.bind(this)
});
this.addView(this._PagedSearchView);
},
_createUsersListView: function () {
return new GroupUsersListView({
users: this._orgUsers
});
},
_initBinds: function () {
this.listenTo(this._orgUsers, 'change:selected', this._onChangeSelected);
this.listenTo(this.model, 'change:state', this.render);
},
_onChangeSelected: function () {
this.$('.ok').toggleClass('is-disabled', this._selectedUsers().length === 0);
},
_selectedUsers: function () {
return this._orgUsers.where({ selected: true });
},
_addUsers: function (password) {
const selectedUsers = this._selectedUsers();
const ids = _.pluck(selectedUsers, 'id');
this._group.users.addInBatch(ids, password)
.done(() => {
this._group.users.add(selectedUsers);
this._modalModel.destroy();
})
.fail(response => {
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this.model.set({
state: 'passwordConfirmationFail',
failMessage: errors || ''
});
}
this.model.set({ state: 'saveFail' });
});
}
});
@@ -0,0 +1,23 @@
<div class="CDB-Text Dialog-content Dialog-content--expanded">
<div class="Dialog-header Dialog-header--expanded CreateDialog-header">
<ul class="CreateDialog-headerSteps">
<li class="CreateDialog-headerStep CreateDialog-headerStep--single">
<div class="Dialog-headerIcon Dialog-headerIcon--neutral">
<i class="CDB-IconFont CDB-IconFont-boss"></i>
</div>
<p class="Dialog-headerTitle">Add users to this group</p>
<p class="Dialog-headerText">When sharing a dataset or map with a group all the users within that group will get the same permissions.</p>
</li>
</ul>
</div>
<div class="CDB-Text js-dlg-body"></div>
<div class="Dialog-stickyFooter">
<div class="Dialog-footer ChangePrivacy-shareFooter u-inner">
<div></div>
<button class="CDB-Button CDB-Button--primary ok is-disabled">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
</div>
</div>
</div>
@@ -0,0 +1,96 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const loadingView = require('builder/components/loading/render-loading');
const createGroupTemplate = require('./create-group.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'onCreated',
'flashMessageModel'
];
/**
* View to create a new group for an organization.
*/
module.exports = CoreView.extend({
tagName: 'form',
events: {
'click .js-create': '_onClickCreate',
'keyup .js-name': '_onChangeName'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this._initBinds();
},
render: function () {
if (this.model.get('isLoading')) {
this.$el.html(
loadingView({
title: 'Creating group'
})
);
} else {
this.$el.html(createGroupTemplate());
}
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isLoading', this.render);
},
_onClickCreate: function (ev) {
this.killEvent(ev);
const name = this._name();
if (name) {
this.model.set('isLoading', true);
this._group.save(
{ display_name: name },
{
wait: true,
success: this._onCreated,
error: this._showErrors.bind(this)
}
);
}
},
_showErrors: function (message, response, request) {
this.model.set('isLoading', false);
let flashMessage = 'Could not create group for some unknown reason, please try again';
let jsonData;
try {
jsonData = response && JSON.parse(response.responseText);
} catch (e) {
jsonData = {};
}
if (jsonData && jsonData.errors) {
flashMessage = jsonData.errors.join('. ');
}
this._flashMessageModel.show(flashMessage, 'error');
},
_onChangeName: function () {
this._flashMessageModel.hide();
this.$('.js-create').toggleClass('is-disabled', this._name().length === 0);
},
_name: function () {
return this.$('.js-name').val();
}
});
@@ -0,0 +1,16 @@
<div class="FormAccount-separator"></div>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor" for="group-name">Group name</label>
</div>
<div class="FormAccount-rowData">
<input type="text" class="CDB-InputText CDB-Text js-name" id="group-name" size="30" maxlength="28" placeholder="Enter a name of group" />
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
</div>
</div>
</div>
<div class="FormAccount-footer u-justifyEnd">
<button class="CDB-Button CDB-Button--primary is-disabled js-create">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Create group</span>
</button>
</div>
@@ -0,0 +1,139 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const loadingView = require('builder/components/loading/render-loading');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const template = require('./edit-group.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'userModel',
'flashMessageModel',
'modals',
'onSaved',
'onDeleted'
];
/**
* View to edit an organization group.
*/
module.exports = CoreView.extend({
tagName: 'form',
events: {
'click .js-delete': '_onClickDelete',
'click .js-save': '_onClickSave',
'submit form': '_onClickSave',
'keyup .js-name': '_onChangeName'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this.model = new Backbone.Model();
this._initBinds();
},
render: function () {
if (this.model.get('isLoading')) {
this.$el.html(
loadingView({
title: this.model.get('loadingText')
})
);
} else {
this.$el.html(
template({ displayName: this._group.get('display_name') })
);
}
return this;
},
_initBinds: function () {
this.listenTo(this.model, 'change:isLoading', this.render);
},
_onClickSave: function (ev) {
this.killEvent(ev);
const name = this._name();
if (name && name !== this._group.get('display_name')) {
this._setLoading('Saving changes');
this._group.save(
{ display_name: name },
{
wait: true,
success: this._onSaved,
error: this._showErrors.bind(this)
});
}
},
_onClickDelete: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._destroyGroup();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._destroyGroup(password)
});
},
_destroyGroup: function (password) {
this._setLoading('Deleting group');
this._group.destroy({
wait: true,
data: JSON.stringify({
...this._group.attributes,
password_confirmation: password
}),
contentType: 'application/json; charset=utf-8',
success: this._onDeleted,
error: this._showErrors.bind(this)
});
},
_setLoading: function (msg) {
this._flashMessageModel.hide();
this.model.set({
isLoading: !!msg,
loadingText: msg
});
},
_showErrors: function (message, response, request) {
this._setLoading('');
let flashMessage = 'Could not update group for some unknown reason, please try again';
let jsonData;
try {
jsonData = response && JSON.parse(response.responseText);
} catch (e) {
jsonData = {};
}
if (jsonData && jsonData.errors) {
flashMessage = jsonData.errors.join('. ');
}
this._flashMessageModel.show(flashMessage, 'error');
},
_onChangeName: function () {
this.$('.js-save').toggleClass('is-disabled', this._name().length === 0);
this._flashMessageModel.hide();
},
_name: function () {
return this.$('.js-name').val();
}
});
@@ -0,0 +1,21 @@
<div class="FormAccount-separator"></div>
<div class="FormAccount-row">
<div class="FormAccount-rowLabel">
<label class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor" for="group-name">Group name</label>
</div>
<div class="FormAccount-rowData">
<input type="text" class="CDB-InputText CDB-Text FormAccount-input FormAccount-input--med js-name" id="group-name" size="30" maxlength="28" value="<%- displayName %>" placeholder="Enter a name of group" />
<div class="FormAccount-rowInfo FormAccount-rowInfo--marginLeft">
</div>
</div>
</div>
<div class="FormAccount-footer">
<button type="submit" class="CDB-Button CDB-Button--error js-delete">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Delete this group</span>
</button>
<button type="submit" class="CDB-Button CDB-Button--primary js-save">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Save changes</span>
</button>
</div>
@@ -0,0 +1,124 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const AddGroupUsersView = require('dashboard/views/organization/groups-admin/add-group-users/add-group-users-view');
const template = require('./add-or-remove-group-users-filters-extra.tpl');
const ViewFactory = require('builder/components/view-factory');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel'
];
/**
* View for the add/remove button in the filters part.
*/
module.exports = CoreView.extend({
className: 'Filters-group',
events: {
'click .js-add-users': '_onClickAddUsers',
'click .js-rm-users': '_onClickRemoveUsers'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
this.listenTo(this._group.users, 'change:selected', this._onChangeSelectedUser);
this.listenTo(this._group.users, 'add remove reset', this.render);
},
render: function () {
this.$el.html(template());
return this;
},
_onClickAddUsers: function (event) {
this.killEvent(event);
this._openAddGroupsUsersDialog();
},
_openAddGroupsUsersDialog: function () {
this._modals.create(modalModel => {
return new AddGroupUsersView({
group: this._group,
orgUsers: this._orgUsers,
modalModel,
userModel: this._userModel
});
});
},
_onChangeSelectedUser: function () {
const hasSelectedUsers = this._selectedUsers().length > 0;
this.$('.js-add-users').toggle(!hasSelectedUsers);
this.$('.js-rm-users').toggle(hasSelectedUsers);
},
_onClickRemoveUsers: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._removeUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._removeUsers(password)
});
},
_removeUsers: function (password) {
const selectedUsers = this._selectedUsers();
if (selectedUsers.length > 0) {
const userIds = _.pluck(selectedUsers, 'id');
const modalModel = this._modals.create(() => {
return this._createLoadingView();
});
this._group.users.removeInBatch(userIds, password)
.always(function () {
modalModel.destroy();
})
.fail(response => {
modalModel.destroy();
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(requestErrorTemplate({ msg: errors }));
});
}
this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(errorTemplate({ msg: errors }));
});
});
}
},
_createLoadingView: function () {
return ViewFactory.createByHTML(loadingView({
title: 'Removing users',
descHTML: randomQuote()
}));
},
_selectedUsers: function () {
return this._group.users.where({ selected: true });
}
});
@@ -0,0 +1,6 @@
<button class="CDB-Button CDB-Button--primary js-add-users">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
<button class="CDB-Button CDB-Button--error js-rm-users" style="display: none;">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Remove from group</span>
</button>
@@ -0,0 +1,101 @@
const _ = require('underscore');
const CoreView = require('backbone/core-view');
const ViewFactory = require('builder/components/view-factory');
const template = require('./empty-group-filters-extra.tpl');
const ModalsServiceModel = require('builder/components/modals/modals-service-model');
const PasswordValidatedForm = require('dashboard/helpers/password-validated-form');
const loadingView = require('builder/components/loading/render-loading.js');
const requestErrorTemplate = require('dashboard/views/data-library/content/request-error-template.tpl');
const errorTemplate = require('dashboard/views/data-library/content/error-template.tpl');
const responseParser = require('dashboard/helpers/response-parser');
const randomQuote = require('builder/components/loading/random-quote');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groupUsers',
'orgUsers',
'userModel'
];
/**
* View for the add users button and state.
*/
module.exports = CoreView.extend({
className: 'Filters-group',
events: {
'click .js-add-users': '_onClickAddUsers'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._modals = new ModalsServiceModel();
// Init binds
this.listenTo(this._orgUsers, 'change:selected', this._onChangeSelectedUser);
},
render: function () {
this.$el.html(template());
return this;
},
_onChangeSelectedUser: function () {
this.$('.js-add-users').toggleClass('is-disabled', this._selectedUsers().length === 0);
},
_onClickAddUsers: function (ev) {
this.killEvent(ev);
if (!this._userModel.needsPasswordConfirmation()) {
return this._addUsers();
}
PasswordValidatedForm.showPasswordModal({
modalService: this._modals,
onPasswordTyped: password => this._addUsers(password)
});
},
_addUsers: function (password) {
const selectedUsers = this._selectedUsers();
if (selectedUsers.length > 0) {
const userIds = _.pluck(selectedUsers, 'id');
const modalModel = this._modals.create(modalModel => {
return this._createLoadingView();
});
this._groupUsers.addInBatch(userIds, password)
.always(() => modalModel.destroy())
.fail(response => {
const errors = responseParser(response) || '';
if (errors.indexOf('Confirmation password') > -1) {
return this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(requestErrorTemplate({ msg: errors }));
});
}
this._modals.create(function (modalModel) {
return ViewFactory.createByHTML(errorTemplate({ msg: errors }));
});
});
}
},
_createLoadingView: function () {
return ViewFactory.createByHTML(loadingView({
title: 'Adding users',
descHTML: randomQuote()
}));
},
_selectedUsers: function () {
return this._orgUsers.where({ selected: true });
}
});
@@ -0,0 +1,3 @@
<button class="CDB-Button CDB-Button--primary is-disabled js-add-users">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Add users</span>
</button>
@@ -0,0 +1,65 @@
const $ = require('jquery');
const CoreView = require('backbone/core-view');
const pluralizeString = require('dashboard/helpers/pluralize');
const template = require('./group-header.tpl');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'urls'
];
/**
* Header view when looking at details of a specific group.
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
_initBinds: function () {
this.listenTo(this._group, 'change:display_name', this.render);
this.listenTo(this._group.users, 'reset add remove', this.render);
},
render: function () {
this._$orgSubheader().hide();
const isNewGroup = this._group.isNew();
const templateData = {
backUrl: this._urls.root,
title: this._group.get('display_name') || 'Create new group',
isNewGroup: isNewGroup,
usersUrl: false
};
if (isNewGroup) {
templateData.editUrl = window.location;
templateData.editUrl.isCurrent = true;
} else {
templateData.editUrl = this._urls.edit;
templateData.usersUrl = this._urls.users;
const usersCount = this._group.users.length;
templateData.usersLabel = usersCount === 0 ? 'Users' : `${usersCount} ${pluralizeString('User', 'Users', usersCount)}`;
if (!this._urls.users.isCurrent) {
templateData.backUrl = this._urls.users;
}
}
this.$el.html(template(templateData));
return this;
},
_$orgSubheader: function () {
return $('.js-org-subheader');
},
clean: function () {
this._$orgSubheader().show();
CoreView.prototype.clean.call(this);
}
});
@@ -0,0 +1,33 @@
<div class="Filters is-relative">
<span class="Filters-separator"></span>
<div class="Filters-inner">
<div class="Filters-row">
<ul class="Filters-group CDB-Text CDB-Size-medium">
<li class="u-flex u-alignCenter">
<a href="<%- backUrl %>" class="u-actionTextColor u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-arrowPrev u-rSpace--xl"></i>
</a>
</li>
<li class="Filters-typeItem">
<div class="FormAccount-title">
<p class="CDB-Text CDB-Size-medium is-semibold u-mainTextColor"><%- title %></p>
</div>
</li>
</ul>
<ul class="Filters-group CDB-Text CDB-Size-medium">
<% if (usersUrl) { %>
<li class="Filters-typeItem">
<a href="<%- usersUrl %>" class="CDB-Text CDB-Size-medium u-mainTextColor Filters-typeLink <%- usersUrl.isCurrent ? 'is-selected' : '' %>">
<%- usersLabel %>
</a>
</li>
<% } %>
<li class="Filters-typeItem">
<a href="<%- editUrl %>" class="CDB-Text CDB-Size-medium u-mainTextColor Filters-typeLink <%- editUrl.isCurrent ? 'is-selected' : '' %>">
Settings
</a>
</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<a href="<%- createGroupUrl %>" class="CDB-Button CDB-Button--primary">
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase">Create new group</span>
</a>
@@ -0,0 +1,66 @@
const CoreView = require('backbone/core-view');
const GroupsListView = require('dashboard/views/organization/groups-admin/groups-list/groups-list-view');
const groupsIndexFiltersExtraTemplate = require('./group-index-filters-extra.tpl');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const ViewFactory = require('builder/components/view-factory');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groups',
'router',
'newGroupUrl'
];
/**
* Index view of groups to list groups of an organization
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
const pagedSearchView = new PagedSearchView({
pagedSearchModel: new PagedSearchModel({
fetch_users: true,
fetch_shared_maps_count: true,
fetch_shared_tables_count: true
}),
collection: this._groups,
createListView: this._createGroupsView.bind(this),
thinFilters: true,
filtersExtrasView: this._createFiltersExtraView(),
noResults: {
icon: 'CDB-IconFont-group',
title: 'You have not created any groups yet',
msg: 'Creating groups enables you to visualize and search for user members assigned to a business group or team in your organization.'
}
});
this.addView(pagedSearchView);
this.$el.empty();
this.$el.append(pagedSearchView.render().el);
return this;
},
_createGroupsView: function () {
return new GroupsListView({
groups: this._groups,
newGroupUrl: this._newGroupUrl
});
},
_createFiltersExtraView: function () {
return ViewFactory.createByTemplate(groupsIndexFiltersExtraTemplate, {
createGroupUrl: this._router._rootUrl.urlToPath('new')
}, {
className: 'Filters-group'
});
}
});
@@ -0,0 +1,54 @@
const CoreView = require('backbone/core-view');
const template = require('./group-user.tpl');
const pluralizeStr = require('dashboard/helpers/pluralize');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'model'
];
/**
* View of a single group user.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'OrganizationList-user is-selectable',
events: {
'click': '_onClick'
},
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._initBinds();
},
_initBinds: function () {
this.listenTo(this.model, 'change:selected', this._onChangeSelected);
},
render: function () {
this.$el.html(
template({
avatarUrl: this.model.get('avatar_url'),
username: this.model.get('username'),
email: this.model.get('email'),
maps_count: pluralizeStr.prefixWithCount('map', 'maps', this.model.get('all_visualization_count')),
table_count: pluralizeStr.prefixWithCount('dataset', 'datasets', this.model.get('table_count'))
})
);
return this;
},
_onChangeSelected: function (model, isSelected) {
this.$el.toggleClass('is-selected', !!isSelected);
},
_onClick: function (ev) {
this.killEvent(ev);
this.model.set('selected', !this.model.get('selected'));
}
});
@@ -0,0 +1,25 @@
<a class="CDB-Text OrganizationList-userLink">
<div class="OrganizationList-userAvatar UserAvatar">
<img src="<%- avatarUrl %>" alt="<%- username %>" src="<%- username %>" class="UserAvatar-img UserAvatar-img--medium-large OrganizationList-userAvatar--img" />
</div>
<div class="OrganizationList-userInfo">
<div class="OrganizationList-userInfoName">
<h3 class="CDB-Size-medium OrganizationList-userInfoTitle u-ellipsLongText" title="<%- username %>">
<%- username %>
</h3>
<h4 class="CDB-Text CDB-Size-small u-ellipis u-secondaryTextColor" title="<%- email %>">
<%- email %>
</h4>
</div>
<div class="OrganizationList-userInfoData CDB-Text CDB-Size-small u-altTextColor u-alignCenter">
<p class="u-flex u-alignCenter">
<i class="CDB-IconFont CDB-IconFont-map OrganizationList-userInfoData--paragraph-icon"></i>
<%- maps_count %>
</p>
<p class="u-flex u-alignCenter u-lSpace--xl">
<i class="CDB-IconFont CDB-IconFont-rows OrganizationList-userInfoData--paragraph-icon"></i>
<%- table_count %>
</p>
</div>
</div>
</a>
@@ -0,0 +1,43 @@
const CoreView = require('backbone/core-view');
const GroupUserView = require('dashboard/views/organization/groups-admin/group-user/group-user-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'users'
];
/**
* View of group users.
*/
module.exports = CoreView.extend({
tagName: 'ul',
className: 'OrganizationList',
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
// init binds
this.listenTo(this._users, 'reset add remove', this.render);
},
render: function () {
this.clearSubViews();
this._renderUsers();
return this;
},
_renderUsers: function () {
this._users.each(this._createUserView, this);
},
_createUserView: function (user) {
const view = new GroupUserView({
model: user
});
this.addView(view);
this.$el.append(view.render().el);
}
});
@@ -0,0 +1,144 @@
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
const PagedSearchView = require('dashboard/components/paged-search/paged-search-view');
const PagedSearchModel = require('dashboard/data/paged-search-model');
const ViewFactory = require('builder/components/view-factory');
const loadingTemplate = require('builder/components/loading/loading.tpl');
const randomQuote = require('builder/components/loading/random-quote');
const AddRemoveFiltersExtraView = require('dashboard/views/organization/groups-admin/filters/add-or-remove-group-users-filters-extra-view');
const EmptyGroupFiltersExtraView = require('dashboard/views/organization/groups-admin/filters/empty-group-filters-extra-view');
const GroupUsersListView = require('dashboard/views/organization/groups-admin/group-users-list/group-users-list-view');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'group',
'orgUsers',
'userModel'
];
/**
* View to manage users of a group
* It basically has two states, each which relies on its own collection:
* - Empty group: i.e. no users, show organization users and allow to add users directly by selecting
* - Group users: allow to add or remove users from group.
*/
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
this._groupUsers = this._group.users;
this._hasPrefetchedGroupUsers = false;
this._orgUsers.excludeCurrentUser(false);
this.model = new Backbone.Model({
hasPrefetchedGroupUsers: false,
lastRendered: null //, 'groupUsers', 'empty'
});
// Init binds
this.listenTo(this._groupUsers, 'sync', this._onResetGroupUsers);
// Pre-fetch to know what view to render
this._groupUsers.fetch({
success: () => {
this.model.set('hasPrefetchedGroupUsers', true);
this.render();
}
});
},
render: function () {
this.clearSubViews();
this.$el.empty();
let view;
if (this.model.get('hasPrefetchedGroupUsers')) {
view = this._groupUsers.totalCount() > 0
? this._createViewForGroupUsers()
: this._createViewForEmptyGroup();
} else {
view = this._createInitialPreloadingView();
}
this.addView(view);
this.$el.append(view.render().el);
return this;
},
clean: function () {
this._orgUsers.restoreExcludeCurrentUser();
CoreView.prototype.clean.apply(this);
},
_createInitialPreloadingView: function () {
return ViewFactory.createByTemplate(loadingTemplate, {
title: 'Getting users',
descHTML: randomQuote()
});
},
_createViewForGroupUsers: function () {
this.model.set('lastRendered', 'groupUsers');
const filtersExtrasView = new AddRemoveFiltersExtraView({
group: this._group,
orgUsers: this._orgUsers,
userModel: this._userModel
});
this.addView(filtersExtrasView);
return new PagedSearchView({
pagedSearchModel: new PagedSearchModel(),
collection: this._groupUsers,
createListView: this._createGroupUsersListView.bind(this, this._groupUsers),
thinFilters: true,
filtersExtrasView
});
},
_createViewForEmptyGroup: function () {
this.model.set('lastRendered', 'empty');
const filtersExtrasView = new EmptyGroupFiltersExtraView({
groupUsers: this._groupUsers,
orgUsers: this._orgUsers,
userModel: this._userModel
});
this.addView(filtersExtrasView);
return new PagedSearchView({
pagedSearchModel: new PagedSearchModel(),
collection: this._orgUsers,
createListView: this._createGroupUsersListView.bind(this, this._orgUsers),
thinFilters: true,
filtersExtrasView
});
},
_createGroupUsersListView: function (usersCollection) {
return new GroupUsersListView({
users: usersCollection
});
},
_onResetGroupUsers: function () {
// just this.render() is not enough, because each sub-view re-renders its view on state changes,
// Instead, only re-render when hitting the edge-cases after a rest
const lastRendered = this.model.get('lastRendered');
const totalGroupUsersCount = this._groupUsers.totalCount();
if (lastRendered === 'empty') {
// scenario: added at least one user, so group is no longer empty => change to group users view to add users
if (totalGroupUsersCount > 0) {
this.render();
}
} else if (lastRendered === 'groupUsers') {
// scenario: removed last group user(s), so the group is now "empty" => change to org users view to add users
if (totalGroupUsersCount === 0) {
this.render();
}
}
}
});
@@ -0,0 +1,40 @@
const CoreView = require('backbone/core-view');
const template = require('./group.tpl');
const pluralizeStr = require('dashboard/helpers/pluralize');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'model',
'url'
];
/**
* View for an individual group.
*/
module.exports = CoreView.extend({
tagName: 'li',
className: 'OrganizationList-user',
_PREVIEW_COUNT: 3,
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
const sharedMapsCount = this.model.get('shared_maps_count');
const sharedDatasetsCount = this.model.get('shared_tables_count');
this.$el.html(
template({
displayName: this.model.get('display_name'),
sharedMapsCount: pluralizeStr('1 shared map', `${sharedMapsCount} shared maps`, sharedMapsCount),
sharedDatasetsCount: pluralizeStr('1 shared dataset', `${sharedDatasetsCount} shared datasets`, sharedDatasetsCount),
url: this._url,
previewUsers: this.model.users.toArray().slice(0, this._PREVIEW_COUNT),
usersCount: Math.max(this.model.users.length - this._PREVIEW_COUNT, 0)
})
);
return this;
}
});
@@ -0,0 +1,26 @@
<a class="CDB-Text OrganizationList-userLink" href="<%- url %>">
<div class="OrganizationList-userInfo">
<div class="OrganizationList-userInfoName">
<h3 class="CDB-Size-medium OrganizationList-userInfoTitle u-ellipsLongText" title="<%- displayName %>">
<%- displayName %>
</h3>
<h4 class="OrganizationList-userInfoSubtitle u-ellipsLongText">
<%- sharedMapsCount %> &bull; <%- sharedDatasetsCount %>
</h4>
</div>
</div>
<div class="OrganizationList-userInfoData">
<% previewUsers.forEach(function(u) { %>
<span class="UserAvatar is-in-list">
<img class="UserAvatar-img UserAvatar-img--medium" src="<%- u.get('avatar_url') %>" title="<%- u.nameOrUsername() %>" />
</span>
<% }) %>
<% if (usersCount > 0) { %>
<span class="UserAvatar is-in-list">
<span class="UserAvatar-img UserAvatar-img--medium UserAvatar-img--textReplacement">
+<%- usersCount %>
</span>
</span>
<% } %>
</div>
</a>
@@ -0,0 +1,41 @@
const CoreView = require('backbone/core-view');
const GroupView = require('dashboard/views/organization/groups-admin/group-view/group-view');
const ViewFactory = require('builder/components/view-factory');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const REQUIRED_OPTS = [
'groups',
'newGroupUrl'
];
module.exports = CoreView.extend({
initialize: function (options) {
checkAndBuildOpts(options, REQUIRED_OPTS, this);
},
render: function () {
this.clearSubViews();
this.$el.empty();
this._renderGroupsView();
return this;
},
_renderGroupsView: function () {
const view = ViewFactory.createListView(this._createGroupViews(), {
tagName: 'ul',
className: 'OrganizationList'
});
this.addView(view);
this.$el.append(view.render().el);
},
_createGroupViews: function () {
return this._groups.map(model => {
return () => new GroupView({
model,
url: this._newGroupUrl(model)
});
});
}
});

Some files were not shown because too many files have changed in this diff Show More