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