'use strict'; /* global angular:false */ /* global showdown:false */ /* global moment:false */ /* global $:false */ // deal with accessToken in the query, this is passed for example on password reset and account setup upon invite var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); if (search.accessToken) { localStorage.token = search.accessToken; // strip the accessToken and expiresAt, then preserve the rest delete search.accessToken; delete search.expiresAt; // this will reload the page as this is not a hash change window.location.search = encodeURIComponent(Object.keys(search).map(function (key) { return key + '=' + search[key]; }).join('&')); } // poor man's async in the global namespace function asyncForEachParallel(items, handler, callback) { var alreadyDone = 0; var errored = false; if (items.length === 0) return callback(); function done(error) { // do nothing if already called back due to error if (errored) return; if (error) { errored = true; return callback(error); } ++alreadyDone; // we are done if (alreadyDone === items.length) callback(); } for (var i = 0; i < items.length; ++i) { handler(items[i], done); } } function asyncForEach(items, handler, callback) { var cur = 0; if (items.length === 0) return callback(); (function iterator() { handler(items[cur], function (error) { if (error) return callback(error); if (cur >= items.length-1) return callback(); ++cur; iterator(); }); })(); } function asyncSeries(funcs, callback) { var cur = 0; if (funcs.length === 0) return callback(); (function iterator() { funcs[cur](function (error) { if (error) return callback(error); if (cur >= funcs.length-1) return callback(); ++cur; iterator(); }); })(); } // create main application module var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']); app.config(['NotificationProvider', function (NotificationProvider) { NotificationProvider.setOptions({ delay: 5000, startTop: 60, positionX: 'left', templateUrl: 'notification.html' }); }]); // configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist app.config(function ($sceDelegateProvider) { $sceDelegateProvider.resourceUrlWhitelist([ // Allow same origin resource loads. 'self', // Allow loading from our assets domain. 'https://cloudron.io/**', 'https://staging.cloudron.io/**', 'https://dev.cloudron.io/**', // Allow local development against the appstore pages 'http://localhost:5000/**' ]); }); // setup all major application routes app.config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/', { redirectTo: '/apps' }).when('/users', { controller: 'UsersController', templateUrl: 'views/users.html?<%= revision %>' }).when('/app/:appId/:view?', { controller: 'AppController', templateUrl: 'views/app.html?<%= revision %>' }).when('/appstore', { controller: 'AppStoreController', templateUrl: 'views/appstore.html?<%= revision %>' }).when('/appstore/:appId', { controller: 'AppStoreController', templateUrl: 'views/appstore.html?<%= revision %>' }).when('/apps', { controller: 'AppsController', templateUrl: 'views/apps.html?<%= revision %>' }).when('/account', { controller: 'AccountController', templateUrl: 'views/account.html?<%= revision %>' }).when('/backups', { controller: 'BackupsController', templateUrl: 'views/backups.html?<%= revision %>' }).when('/graphs', { controller: 'GraphsController', templateUrl: 'views/graphs.html?<%= revision %>' }).when('/domains', { controller: 'DomainsController', templateUrl: 'views/domains.html?<%= revision %>' }).when('/email', { controller: 'EmailController', templateUrl: 'views/email.html?<%= revision %>' }).when('/email/:domain', { controller: 'EmailController', templateUrl: 'views/email.html?<%= revision %>' }).when('/notifications', { controller: 'NotificationsController', templateUrl: 'views/notifications.html?<%= revision %>' }).when('/settings', { controller: 'SettingsController', templateUrl: 'views/settings.html?<%= revision %>' }).when('/activity', { controller: 'ActivityController', templateUrl: 'views/activity.html?<%= revision %>' }).when('/support', { controller: 'SupportController', templateUrl: 'views/support.html?<%= revision %>' }).when('/system', { controller: 'SystemController', templateUrl: 'views/system.html?<%= revision %>' }).when('/tokens', { controller: 'TokensController', templateUrl: 'views/tokens.html?<%= revision %>' }).otherwise({ redirectTo: '/'}); }]); // keep in sync with appdb.js var ISTATES = { PENDING_INSTALL: 'pending_install', PENDING_CLONE: 'pending_clone', PENDING_CONFIGURE: 'pending_configure', PENDING_UNINSTALL: 'pending_uninstall', PENDING_RESTORE: 'pending_restore', PENDING_UPDATE: 'pending_update', PENDING_BACKUP: 'pending_backup', PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change PENDING_LOCATION_CHANGE: 'pending_location_change', PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration', PENDING_RESIZE: 'pending_resize', PENDING_DEBUG: 'pending_debug', ERROR: 'error', INSTALLED: 'installed' }; var HSTATES = { HEALTHY: 'healthy', UNHEALTHY: 'unhealthy', ERROR: 'error', DEAD: 'dead' }; app.filter('installError', function () { return function (app) { if (app.installationState === ISTATES.ERROR) return true; if (app.installationState === ISTATES.INSTALLED) { // app.health can also be null to indicate insufficient data if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) return true; } return false; }; }); app.filter('activeTask', function () { return function (app) { return app.taskId !== null; }; }); app.filter('installSuccess', function () { return function (app) { return app.installationState === ISTATES.INSTALLED; }; }); app.filter('appIsInstalledAndHealthy', function () { return function (app) { return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY); }; }); app.filter('activeOAuthClients', function () { return function (clients, user) { return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); }); }; }); // this appears when an item in app grid is clicked app.filter('prettyAppErrorMessage', function () { return function (app) { if (!app) return ''; if (app.installationState === ISTATES.INSTALLED) { // app.health can also be null to indicate insufficient data if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.'; } if (app.error.reason === 'Access Denied') { if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.'; } else if (app.error.reason === 'Already Exists') { if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.'; } return app.error.message; }; }); // this appears as tool tip in app grid app.filter('appProgressMessage', function () { return function (app) { var message = app.message || (app.error ? app.error.message : ''); return message; }; }); app.filter('selectedTagFilter', function () { return function selectedTagFilter(apps, selectedTags) { return apps.filter(function (app) { if (selectedTags.length === 0) return true; if (!app.tags) return false; for (var i = 0; i < selectedTags.length; i++) { if (app.tags.indexOf(selectedTags[i]) === -1) return false; } return true; }); }; }); app.filter('selectedDomainFilter', function () { return function selectedDomainFilter(apps, selectedDomain) { return apps.filter(function (app) { if (!selectedDomain) return true; if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN if (selectedDomain.domain === app.domain) return true; return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; }); }); }; }); app.filter('prettyDomains', function () { return function prettyDomains(domains) { return domains.map(function (d) { return d.domain; }).join(', '); }; }); app.filter('prettyMemory', function () { return function (memory) { // Adjust the default memory limit if it changes return memory ? Math.floor(memory / 1024 / 1024) : 256; }; }); app.filter('prettyDiskSize', function () { return function (size) { if (!size) return 'not available yet'; var i = Math.floor(Math.log(size) / Math.log(1024)); return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]; }; }); app.filter('installationActive', function () { return function (app) { if (app.installationState === ISTATES.ERROR) return false; if (app.installationState === ISTATES.INSTALLED) return false; return true; }; }); // this appears in the app grid app.filter('installationStateLabel', function () { return function(app, user) { if (!app) return ''; var waiting = app.progress === 0 ? ' (Queued)' : ''; switch (app.installationState) { case ISTATES.PENDING_INSTALL: case ISTATES.PENDING_CLONE: return 'Installing' + waiting; case ISTATES.PENDING_LOCATION_CHANGE: case ISTATES.PENDING_CONFIGURE: case ISTATES.PENDING_RECREATE_CONTAINER: case ISTATES.PENDING_DEBUG: case ISTATES.PENDING_RESIZE: return 'Configuring' + waiting; case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'Migrating data' + waiting; case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting; case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting; case ISTATES.PENDING_UPDATE: return 'Updating' + waiting; case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting; case ISTATES.ERROR: { if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error'; return 'Error'; } case ISTATES.INSTALLED: { if (app.debugMode) { return app.debugMode.readonlyRootfs ? 'Paused (Repair)' : 'Paused (Debug)'; } else if (app.runState === 'running') { if (!app.health) return 'Starting...'; // no data yet if (app.health === HSTATES.HEALTHY) return 'Running'; return 'Not responding'; // dead/exit/unhealthy } else if (app.runState === 'pending_start') return 'Starting...'; else if (app.runState === 'pending_stop') return 'Stopping...'; else if (app.runState === 'stopped') return 'Stopped'; else return app.runState; } default: return app.installationState; } }; }); app.filter('readyToUpdate', function () { return function (apps) { return apps.every(function (app) { return (app.installationState === ISTATES.ERROR) || (app.installationState === ISTATES.INSTALLED); }); }; }); app.filter('inProgressApps', function () { return function (apps) { return apps.filter(function (app) { return app.installationState !== ISTATES.ERROR && app.installationState !== ISTATES.INSTALLED; }); }; }); app.filter('applicationLink', function() { return function(app) { if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && !app.pendingPostInstallConfirmation) { return 'https://' + app.fqdn; } else { return ''; } }; }); app.filter('prettyHref', function () { return function (input) { if (!input) return input; if (input.indexOf('http://') === 0) return input.slice('http://'.length); if (input.indexOf('https://') === 0) return input.slice('https://'.length); return input; }; }); app.filter('prettyDate', function () { // http://ejohn.org/files/pretty.js return function prettyDate(time) { var date = new Date(time), diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew day_diff = Math.floor(diff / 86400); if (isNaN(day_diff) || day_diff < 0) return 'just now'; return day_diff === 0 && ( diff < 60 && 'just now' || diff < 120 && '1 minute ago' || diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' || diff < 7200 && '1 hour ago' || diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') || day_diff === 1 && 'Yesterday' || day_diff < 7 && day_diff + ' days ago' || day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' || day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' || Math.round( day_diff / 365 ) + ' years ago'; }; }); app.filter('prettyLongDate', function () { return function prettyLongDate(time) { return moment(time).format('MMMM Do YYYY, h:mm:ss a'); }; }); app.filter('prettyShortDate', function () { return function prettyShortDate(time) { return moment(time).format('MMMM Do YYYY'); }; }); app.filter('markdown2html', function () { var converter = new showdown.Converter({ extensions: [], simplifiedAutoLink: true, strikethrough: true, tables: true }); return function (text) { return converter.makeHtml(text); }; }); app.filter('postInstallMessage', function () { var SSO_MARKER = '=== sso ==='; return function (text, app) { if (!text) return ''; if (!app) return text; var parts = text.split(SSO_MARKER); if (parts.length === 1) { // [^] matches even newlines. '?' makes it non-greedy if (app.sso) return text.replace(/\[^]*?\<\/nosso\>/g, ''); else return text.replace(/\[^]*?\<\/sso\>/g, ''); } if (app.sso) return parts[1]; else return parts[0]; }; }); // keep this in sync with eventlog.js and CLI tool var ACTION_ACTIVATE = 'cloudron.activate'; var ACTION_PROVISION = 'cloudron.provision'; var ACTION_RESTORE = 'cloudron.restore'; var ACTION_APP_CLONE = 'app.clone'; var ACTION_APP_CONFIGURE = 'app.configure'; var ACTION_APP_INSTALL = 'app.install'; var ACTION_APP_RESTORE = 'app.restore'; var ACTION_APP_UNINSTALL = 'app.uninstall'; var ACTION_APP_UPDATE = 'app.update'; var ACTION_APP_LOGIN = 'app.login'; var ACTION_APP_OOM = 'app.oom'; var ACTION_APP_UP = 'app.up'; var ACTION_APP_DOWN = 'app.down'; var ACTION_BACKUP_FINISH = 'backup.finish'; var ACTION_BACKUP_START = 'backup.start'; var ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start'; var ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish'; var ACTION_CERTIFICATE_NEW = 'certificate.new'; var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew'; var ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update'; var ACTION_DOMAIN_ADD = 'domain.add'; var ACTION_DOMAIN_UPDATE = 'domain.update'; var ACTION_DOMAIN_REMOVE = 'domain.remove'; var ACTION_START = 'cloudron.start'; var ACTION_UPDATE = 'cloudron.update'; var ACTION_USER_ADD = 'user.add'; var ACTION_USER_LOGIN = 'user.login'; var ACTION_USER_REMOVE = 'user.remove'; var ACTION_USER_UPDATE = 'user.update'; var ACTION_USER_TRANSFER = 'user.transfer'; var ACTION_MAIL_ENABLED = 'mail.enabled'; var ACTION_MAIL_DISABLED = 'mail.disabled'; var ACTION_MAIL_MAILBOX_ADD = 'mail.box.add'; var ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove'; var ACTION_MAIL_LIST_ADD = 'mail.list.add'; var ACTION_MAIL_LIST_REMOVE = 'mail.list.remove'; var ACTION_DYNDNS_UPDATE = 'dyndns.update'; var ACTION_SYSTEM_CRASH = 'system.crash'; app.filter('eventLogSource', ['Client', function (Client) { return function(eventLog) { var source = eventLog.source; var line = ''; line = source.username || source.userId || source.authType || 'system'; if (source.appId) { var app = Client.getCachedAppSync(source.appId); line += ' - ' + (app ? app.fqdn : source.appId); } else if (source.ip) { line += ' - ' + source.ip; } return line; }; }]); app.filter('eventLogDetails', function() { // NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well return function(eventLog) { var data = eventLog.data; var errorMessage = data.errorMessage; var details; switch (eventLog.action) { case ACTION_ACTIVATE: return 'Cloudron was activated'; case ACTION_PROVISION: return 'Cloudron was setup'; case ACTION_RESTORE: return 'Cloudron was restored using backup ' + data.backupId; case ACTION_APP_CONFIGURE: if (!data.app) return ''; var q = function (x) { return '"' + x + '"'; }; var name = (data.app.label || data.app.fqdn || data.app.location) + ' (' + data.app.manifest.title + ')'; if ('accessRestriction' in data) { // since it can be null return 'Access restriction of ' + name + ' was changed'; } else if (data.label) { return 'Label of ' + name + ' was set to ' + q(data.label); } else if (data.tags) { return 'Tags of ' + name + ' was set to ' + q(data.tags.join(',')); } else if (data.icon) { return 'Icon of ' + name + ' was changed'; } else if (data.memoryLimit) { return 'Memory limit of ' + name + ' was set to ' + data.memoryLimit; } else if (data.env) { return 'Env vars of ' + name + ' was changed'; } else if ('debugMode' in data) { // since it can be null if (data.debugMode) { return name + ' was placed in repair mode'; } else { return name + ' was taken out of repair mode'; } } else if ('mailboxName' in data) { if (data.mailboxName) { return 'Mailbox of ' + name + ' was set to ' + q(data.mailboxName); } else { return 'Mailbox of ' + name + ' was reset'; } } else if ('enableBackup' in data) { return 'Automatic backups of ' + name + ' was ' + (data.enableBackup ? 'enabled' : 'disabled'); } else if ('enableAutomaticUpdate' in data) { return 'Automatic updates of ' + name + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled'); } else if ('robotsTxt' in data) { if (data.robotsTxt) { return 'robots.txt of ' + name + ' was set'; } else { return 'robots.txt of ' + name + ' was reset'; } } else if ('cert' in data) { if (data.cert) { return 'Custom certificate was set for ' + name; } else { return 'Certificate of ' + name + ' was reset'; } } else if (data.location) { if (data.location !== data.app.location || data.domain !== data.app.domain) { return 'Location of ' + name + ' was set to ' + data.location + '.' + data.domain; } else if (!angular.equals(data.alternateDomains, data.app.alternateDomains)) { return 'Alternate domains of ' + name + ' was set to ' + q(data.alternateDomains.join(',')); } else if (!angular.equals(data.portBindings, data.app.portBindings)) { return 'Port bindings ' + name + ' was set'; } } else if ('dataDir' in data) { if (data.dataDir) { return 'Data directory of ' + name + ' was set ' + data.dataDir; } else { return 'Data directory of ' + name + ' was reset'; } } return data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location); case ACTION_APP_INSTALL: if (!data.app) return ''; return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed at ' + (data.app.fqdn || data.app.location); case ACTION_APP_RESTORE: if (!data.app) return ''; details = data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location); // older versions (<3.5) did not have these fields if (data.fromManifest) details += ' from version ' + data.fromManifest.version; if (data.toManifest) details += ' to version ' + data.toManifest.version; if (data.backupId) details += ' using backup ' + data.backupId; return details; case ACTION_APP_UNINSTALL: if (!data.app) return ''; return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was uninstalled at ' + (data.app.fqdn || data.app.location); case ACTION_APP_UPDATE: if (!data.app) return ''; return data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location) + ' was updated from v' + data.fromManifest.version + ' to v' + data.toManifest.version; case ACTION_APP_CLONE: return data.newApp.manifest.title + ' at ' + (data.newApp.fqdn || data.newApp.location) + ' was cloned from ' + (data.oldApp.fqdn || data.oldApp.location) + ' using backup ' + data.backupId + ' with v' + data.oldApp.manifest.version; case ACTION_APP_LOGIN: return 'App ' + data.appId + ' logged in'; case ACTION_APP_OOM: return data.app.manifest.title + ' ran out of memory'; case ACTION_APP_DOWN: return data.app.manifest.title + ' is down'; case ACTION_APP_UP: return data.app.manifest.title + ' is back online'; case ACTION_BACKUP_START: return 'Backup started'; case ACTION_BACKUP_FINISH: return 'Backup finished' + (errorMessage ? (' error: ' + errorMessage) : ''); case ACTION_BACKUP_CLEANUP_START: return 'Backup cleaner started'; case ACTION_BACKUP_CLEANUP_FINISH: return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + data.removedBoxBackups.length + ' backups'; case ACTION_CERTIFICATE_NEW: return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); case ACTION_DASHBOARD_DOMAIN_UPDATE: return 'Dashboard domain set to ' + data.fqdn; case ACTION_DOMAIN_ADD: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added'; case ACTION_DOMAIN_UPDATE: return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated'; case ACTION_DOMAIN_REMOVE: return 'Domain ' + data.domain + ' was removed'; case ACTION_MAIL_ENABLED: return 'Cloudron Mail was enabled for domain ' + data.domain; case ACTION_MAIL_DISABLED: return 'Cloudron Mail was disabled for domain ' + data.domain; case ACTION_MAIL_MAILBOX_ADD: return 'Mailbox with name ' + data.name + ' was added in domain ' + data.domain; case ACTION_MAIL_MAILBOX_REMOVE: return 'Mailbox with name ' + data.name + ' was removed in domain ' + data.domain; case ACTION_MAIL_LIST_ADD: return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain; case ACTION_MAIL_LIST_REMOVE: return 'Mail list with name ' + data.name + ' was added in domain ' + data.domain; case ACTION_START: return 'Cloudron started with version ' + data.version; case ACTION_UPDATE: return 'Cloudron was updated to version ' + data.boxUpdateInfo.version; case ACTION_USER_ADD: return data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added'; case ACTION_USER_UPDATE: return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated'; case ACTION_USER_REMOVE: return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed'; case ACTION_USER_TRANSFER: return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId; case ACTION_USER_LOGIN: return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' logged in'; case ACTION_DYNDNS_UPDATE: return 'DNS was updated from ' + data.fromIp + ' to ' + data.toIp; case ACTION_SYSTEM_CRASH: return 'A system process crashed'; default: return eventLog.action; } }; }); app.filter('eventLogAction', function() { return function(eventLog) { switch (eventLog.action) { case ACTION_ACTIVATE: return 'Cloudron activated'; case ACTION_RESTORE: return 'Cloudron restored'; case ACTION_PROVISION: return 'Cloudron provisioned'; case ACTION_APP_CONFIGURE: return 'App configured'; case ACTION_APP_INSTALL: return 'App installed'; case ACTION_APP_RESTORE: return 'App restored'; case ACTION_APP_UNINSTALL: return 'App uninstalled'; case ACTION_APP_UPDATE: return 'App updated'; case ACTION_APP_CLONE: return 'App cloned'; case ACTION_APP_LOGIN: return 'App login'; case ACTION_BACKUP_START: return 'Backup started'; case ACTION_BACKUP_FINISH: return 'Backup finished'; case ACTION_BACKUP_CLEANUP_START: return 'Backup cleaner started'; case ACTION_BACKUP_CLEANUP_FINISH: return 'Backup cleaner finished'; case ACTION_CERTIFICATE_NEW: return 'Certificated installed'; case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal'; case ACTION_DASHBOARD_DOMAIN_UPDATE: return 'Dashboard domain updated'; case ACTION_DOMAIN_ADD: return 'Domain added'; case ACTION_DOMAIN_UPDATE: return 'Domain updated'; case ACTION_DOMAIN_REMOVE: return 'Domain removed'; case ACTION_MAIL_ENABLED: return 'Mail enabled'; case ACTION_MAIL_DISABLED: return 'Mail disabled'; case ACTION_MAIL_MAILBOX_ADD: return 'Mailbox added'; case ACTION_MAIL_MAILBOX_REMOVE: return 'Mailbox removed'; case ACTION_MAIL_LIST_ADD: return 'Mail list added'; case ACTION_MAIL_LIST_REMOVE: return 'Mail list removed'; case ACTION_START: return 'Cloudron started'; case ACTION_UPDATE: return 'Cloudron updated'; case ACTION_USER_ADD: return 'User added'; case ACTION_USER_LOGIN: return 'User login'; case ACTION_USER_REMOVE: return 'User removed'; case ACTION_USER_UPDATE: return 'User updated'; case ACTION_DYNDNS_UPDATE: return 'DNS Updated'; default: return eventLog.action; } }; }); // custom directive for dynamic names in forms // See http://stackoverflow.com/questions/23616578/issue-registering-form-control-with-interpolated-name#answer-23617401 app.directive('laterName', function () { // (2) return { restrict: 'A', require: ['?ngModel', '^?form'], // (3) link: function postLink(scope, elem, attrs, ctrls) { attrs.$set('name', attrs.laterName); var modelCtrl = ctrls[0]; // (3) var formCtrl = ctrls[1]; // (3) if (modelCtrl && formCtrl) { modelCtrl.$name = attrs.name; // (4) formCtrl.$addControl(modelCtrl); // (2) scope.$on('$destroy', function () { formCtrl.$removeControl(modelCtrl); // (5) }); } } }; }); app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $location) { var original = $location.path; $location.path = function (path, reload) { if (reload === false) { var lastRoute = $route.current; var un = $rootScope.$on('$locationChangeSuccess', function () { $route.current = lastRoute; un(); }); } return original.apply($location, [path]); }; }]); app.directive('ngClickSelect', function () { return { restrict: 'AC', link: function (scope, element/*, attrs */) { element.bind('click', function () { var selection = window.getSelection(); var range = document.createRange(); range.selectNodeContents(this); selection.removeAllRanges(); selection.addRange(range); }); } }; }); app.directive('ngClickReveal', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.addClass('hand'); var value = ''; scope.$watch(attrs.ngClickReveal, function (newValue, oldValue) { if (newValue !== oldValue) { element.html('hidden'); value = newValue; } }); element.bind('click', function () { element.text(value); }); } }; }); // https://codepen.io/webmatze/pen/isuHh app.directive('tagInput', function () { return { restrict: 'E', scope: { inputTags: '=taglist' }, link: function ($scope, element, attrs) { $scope.defaultWidth = 200; $scope.tagText = ''; // current tag being edited $scope.placeholder = attrs.placeholder; $scope.tagArray = function () { if ($scope.inputTags === undefined) { return []; } return $scope.inputTags.split(',').filter(function (tag) { return tag !== ''; }); }; $scope.addTag = function () { var tagArray; if ($scope.tagText.length === 0) { return; } tagArray = $scope.tagArray(); tagArray.push($scope.tagText); $scope.inputTags = tagArray.join(','); return $scope.tagText = ''; }; $scope.deleteTag = function (key) { var tagArray; tagArray = $scope.tagArray(); if (tagArray.length > 0 && $scope.tagText.length === 0 && key === undefined) { tagArray.pop(); } else { if (key !== undefined) { tagArray.splice(key, 1); } } return $scope.inputTags = tagArray.join(','); }; $scope.$watch('tagText', function (newVal, oldVal) { var tempEl; if (!(newVal === oldVal && newVal === undefined)) { tempEl = $('' + newVal + '').appendTo('body'); $scope.inputWidth = tempEl.width() + 5; if ($scope.inputWidth < $scope.defaultWidth) { $scope.inputWidth = $scope.defaultWidth; } return tempEl.remove(); } }); element.bind('keydown', function (e) { var key = e.which; if (key === 9 || key === 13) { e.preventDefault(); } if (key === 8) { return $scope.$apply('deleteTag()'); } }); element.bind('keyup', function (e) { var key = e.which; if (key === 9 || key === 13 || key === 32 || key === 188) { e.preventDefault(); return $scope.$apply('addTag()'); } }); }, template: '
' + '
' + '{{tag}}' + '
×
' + '
' + '' + '
' }; }); app.config(['fitTextConfigProvider', function (fitTextConfigProvider) { fitTextConfigProvider.config = { loadDelay: 250, compressor: 0.9, min: 8, max: 24 }; }]);