851 lines
32 KiB
JavaScript
851 lines
32 KiB
JavaScript
'use strict';
|
|
|
|
/* global $, async, angular, redirectIfNeeded */
|
|
/* global ERROR,ISTATES,HSTATES,RSTATES,APP_TYPES,NOTIFICATION_TYPES */
|
|
|
|
// 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('&'));
|
|
}
|
|
|
|
// create main application module
|
|
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', '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('/usersettings', {
|
|
controller: 'UserSettingsController',
|
|
templateUrl: 'views/user-settings.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('/profile', {
|
|
controller: 'ProfileController',
|
|
templateUrl: 'views/profile.html?<%= revision %>'
|
|
}).when('/backups', {
|
|
controller: 'BackupsController',
|
|
templateUrl: 'views/backups.html?<%= revision %>'
|
|
}).when('/branding', {
|
|
controller: 'BrandingController',
|
|
templateUrl: 'views/branding.html?<%= revision %>'
|
|
}).when('/network', {
|
|
controller: 'NetworkController',
|
|
templateUrl: 'views/network.html?<%= revision %>'
|
|
}).when('/domains', {
|
|
controller: 'DomainsController',
|
|
templateUrl: 'views/domains.html?<%= revision %>'
|
|
}).when('/email', {
|
|
controller: 'EmailsController',
|
|
templateUrl: 'views/emails.html?<%= revision %>'
|
|
}).when('/emails-eventlog', {
|
|
controller: 'EmailsEventlogController',
|
|
templateUrl: 'views/emails-eventlog.html?<%= revision %>'
|
|
}).when('/emails-queue', {
|
|
controller: 'EmailsQueueController',
|
|
templateUrl: 'views/emails-queue.html?<%= revision %>'
|
|
}).when('/email/:domain/:view?', {
|
|
controller: 'EmailController',
|
|
templateUrl: 'views/email.html?<%= revision %>'
|
|
}).when('/notifications', {
|
|
controller: 'NotificationsController',
|
|
templateUrl: 'views/notifications.html?<%= revision %>'
|
|
}).when('/oidc', {
|
|
redirectTo: '/usersettings'
|
|
}).when('/settings', {
|
|
controller: 'SettingsController',
|
|
templateUrl: 'views/settings.html?<%= revision %>'
|
|
}).when('/eventlog', {
|
|
controller: 'EventLogController',
|
|
templateUrl: 'views/eventlog.html?<%= revision %>'
|
|
}).when('/support', {
|
|
controller: 'SupportController',
|
|
templateUrl: 'views/support.html?<%= revision %>'
|
|
}).when('/system', {
|
|
controller: 'SystemController',
|
|
templateUrl: 'views/system.html?<%= revision %>'
|
|
}).when('/services', {
|
|
controller: 'ServicesController',
|
|
templateUrl: 'views/services.html?<%= revision %>'
|
|
}).when('/volumes', {
|
|
controller: 'VolumesController',
|
|
templateUrl: 'views/volumes.html?<%= revision %>'
|
|
}).otherwise({ redirectTo: '/'});
|
|
}]);
|
|
|
|
app.filter('notificationTypeToColor', function () {
|
|
return function (n) {
|
|
switch (n.type) {
|
|
case NOTIFICATION_TYPES.ALERT_REBOOT:
|
|
case NOTIFICATION_TYPES.ALERT_APP_OOM:
|
|
case NOTIFICATION_TYPES.ALERT_MAIL_STATUS:
|
|
case NOTIFICATION_TYPES.ALERT_CERTIFICATE_RENEWAL_FAILED:
|
|
case NOTIFICATION_TYPES.ALERT_DISK_SPACE:
|
|
case NOTIFICATION_TYPES.ALERT_BACKUP_CONFIG:
|
|
case NOTIFICATION_TYPES.ALERT_BACKUP_FAILED:
|
|
return '#ff4c4c';
|
|
case NOTIFICATION_TYPES.ALERT_BOX_UPDATE:
|
|
case NOTIFICATION_TYPES.ALERT_MANUAL_APP_UPDATE:
|
|
return '#f0ad4e';
|
|
default:
|
|
return '#2196f3';
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('capitalize', function () {
|
|
return function (s) {
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
};
|
|
});
|
|
|
|
app.filter('activeTask', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return app.taskId !== null;
|
|
};
|
|
});
|
|
|
|
app.filter('installSuccess', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return app.installationState === ISTATES.INSTALLED;
|
|
};
|
|
});
|
|
|
|
app.filter('appIsInstalledAndHealthy', function () {
|
|
return function (app) {
|
|
if (!app) return false;
|
|
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
|
|
};
|
|
});
|
|
|
|
app.filter('applicationLink', function () {
|
|
return function(app) {
|
|
if (!app) return '';
|
|
|
|
// app links have http already in the fqdn
|
|
if (app.fqdn.indexOf('http') !== 0) return 'https://' + app.fqdn;
|
|
|
|
return app.fqdn;
|
|
};
|
|
});
|
|
|
|
app.filter('userManagementFilter', function () {
|
|
return function(apps, option) {
|
|
return apps.filter(function (app) {
|
|
if (option.id === '') return true;
|
|
if (option.id === 'sso') return !!(app.manifest.optionalSso || app.manifest.addons.ldap || app.manifest.addons.proxyAuth);
|
|
if (option.id === 'nosso') return app.manifest.optionalSso || (!app.manifest.addons.ldap && !app.manifest.addons.proxyAuth);
|
|
if (option.id === 'email') return !!app.manifest.addons.email;
|
|
|
|
return false;
|
|
});
|
|
};
|
|
});
|
|
|
|
// 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;
|
|
};
|
|
});
|
|
|
|
// see apps.js $scope.states
|
|
app.filter('selectedStateFilter', ['Client', function (Client) {
|
|
return function selectedStateFilter(apps, selectedState) {
|
|
return apps.filter(function (app) {
|
|
if (!selectedState || !selectedState.state) return true;
|
|
|
|
if (selectedState.state === 'running') return app.runState === RSTATES.RUNNING && app.health === HSTATES.HEALTHY && app.installationState === ISTATES.INSTALLED;
|
|
if (selectedState.state === 'stopped') return app.runState === RSTATES.STOPPED;
|
|
if (selectedState.state === 'update_available') return !!(Client.getConfig().update[app.id] && Client.getConfig().update[app.id].manifest.version && Client.getConfig().update[app.id].manifest.version !== app.manifest.version);
|
|
|
|
return app.runState === RSTATES.RUNNING && (app.health !== HSTATES.HEALTHY || app.installationState !== ISTATES.INSTALLED); // not responding
|
|
});
|
|
};
|
|
}]);
|
|
|
|
app.filter('selectedGroupAccessFilter', function () {
|
|
return function selectedGroupAccessFilter(apps, group) {
|
|
return apps.filter(function (app) {
|
|
if (!group.id) return true; // case for no filter entry
|
|
if (!app.accessRestriction) return true;
|
|
|
|
if (!app.accessRestriction.groups) return false;
|
|
|
|
if (app.accessRestriction.groups.indexOf(group.id) !== -1) return true;
|
|
|
|
return false;
|
|
});
|
|
};
|
|
});
|
|
|
|
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._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
|
|
if (app.type === APP_TYPES.LINK) return false;
|
|
|
|
if (selectedDomain.domain === app.domain) return true;
|
|
if (app.aliasDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
|
if (app.redirectDomains.find(function (ad) { return ad.domain === selectedDomain.domain; })) return true;
|
|
|
|
return false;
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('appSearchFilter', function () {
|
|
return function appSearchFilter(apps, appSearch) {
|
|
return apps.filter(function (app) {
|
|
if (!appSearch) return true;
|
|
appSearch = appSearch.toLowerCase();
|
|
return app.fqdn.indexOf(appSearch) !== -1
|
|
|| (app.label && app.label.toLowerCase().indexOf(appSearch) !== -1)
|
|
|| (app.manifest.title && app.manifest.title.toLowerCase().indexOf(appSearch) !== -1)
|
|
|| (appSearch.length >=6 && app.id.indexOf(appSearch) !== -1);
|
|
});
|
|
};
|
|
});
|
|
|
|
app.filter('prettyDomains', function () {
|
|
return function prettyDomains(domains) {
|
|
return domains.map(function (d) { return d.domain; }).join(', ');
|
|
};
|
|
});
|
|
|
|
app.filter('installationActive', function () {
|
|
return function (app) {
|
|
if (app.installationState === ISTATES.ERROR) return false;
|
|
if (app.installationState === ISTATES.INSTALLED) return false;
|
|
return true;
|
|
};
|
|
});
|
|
|
|
// color indicator in app list
|
|
app.filter('installationStateClass', function () {
|
|
const ERROR_CLASS = 'status-error';
|
|
const BUSY_CLASS = 'status-starting fa-beat-fade';
|
|
const INACTIVE_CLASS = 'status-inactive';
|
|
const ACTIVE_CLASS = 'status-active';
|
|
|
|
return function(app) {
|
|
if (!app) return '';
|
|
|
|
switch (app.installationState) {
|
|
case ISTATES.ERROR: return ERROR_CLASS;
|
|
case ISTATES.INSTALLED: {
|
|
if (app.debugMode) {
|
|
return INACTIVE_CLASS;
|
|
} else {
|
|
if (app.runState === RSTATES.RUNNING) {
|
|
if (!app.health) return BUSY_CLASS; // no data yet
|
|
if (app.type === APP_TYPES.LINK || app.health === HSTATES.HEALTHY) return ACTIVE_CLASS;
|
|
return ERROR_CLASS; // dead/exit/unhealthy
|
|
} else {
|
|
return INACTIVE_CLASS;
|
|
}
|
|
}
|
|
}
|
|
default: return BUSY_CLASS;
|
|
}
|
|
};
|
|
});
|
|
|
|
// this appears in the app grid
|
|
app.filter('installationStateLabel', function () {
|
|
return function(app) {
|
|
if (!app) return '';
|
|
|
|
var waiting = app.progress === 0 ? ' (Queued)' : '';
|
|
|
|
switch (app.installationState) {
|
|
case ISTATES.PENDING_INSTALL:
|
|
return 'Installing' + waiting;
|
|
case ISTATES.PENDING_CLONE:
|
|
return 'Cloning' + waiting;
|
|
case ISTATES.PENDING_LOCATION_CHANGE:
|
|
case ISTATES.PENDING_CONFIGURE:
|
|
case ISTATES.PENDING_RECREATE_CONTAINER:
|
|
case ISTATES.PENDING_SERVICES_CHANGE:
|
|
case ISTATES.PENDING_DEBUG:
|
|
return 'Configuring' + waiting;
|
|
case ISTATES.PENDING_RESIZE:
|
|
return 'Resizing' + 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_IMPORT: return 'Importing' + waiting;
|
|
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
|
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
|
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
|
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
|
|
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
|
|
case ISTATES.ERROR: {
|
|
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
|
|
return 'Error';
|
|
}
|
|
case ISTATES.INSTALLED: {
|
|
if (app.debugMode) {
|
|
return 'Recovery Mode';
|
|
} else if (app.runState === RSTATES.RUNNING) {
|
|
if (!app.health) return 'Starting...'; // no data yet
|
|
if (app.type === APP_TYPES.LINK) return '';
|
|
if (app.health === HSTATES.HEALTHY) return 'Running';
|
|
return 'Not responding'; // dead/exit/unhealthy
|
|
} else if (app.runState === RSTATES.STOPPED) return 'Stopped';
|
|
else return app.runState;
|
|
}
|
|
default: return app.installationState;
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('taskName', function () {
|
|
return function(installationState) {
|
|
switch (installationState) {
|
|
case ISTATES.PENDING_INSTALL: return 'install';
|
|
case ISTATES.PENDING_CLONE: return 'clone';
|
|
case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
|
|
case ISTATES.PENDING_CONFIGURE: return 'configure';
|
|
case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
|
|
case ISTATES.PENDING_DEBUG: return 'debug';
|
|
case ISTATES.PENDING_RESIZE: return 'resize';
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
|
|
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
|
|
case ISTATES.PENDING_RESTORE: return 'restore';
|
|
case ISTATES.PENDING_IMPORT: return 'import';
|
|
case ISTATES.PENDING_UPDATE: return 'update';
|
|
case ISTATES.PENDING_BACKUP: return 'backup';
|
|
case ISTATES.PENDING_START: return 'start app';
|
|
case ISTATES.PENDING_STOP: return 'stop app';
|
|
case ISTATES.PENDING_RESTART: return 'restart app';
|
|
default: return installationState || '';
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('errorSuggestion', function () {
|
|
return function (error) {
|
|
if (!error) return '';
|
|
|
|
switch (error.reason) {
|
|
case ERROR.ACCESS_DENIED:
|
|
if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
|
|
return '';
|
|
case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
|
|
case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
|
|
case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
|
|
case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
|
|
case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
|
|
case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
|
|
case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
|
|
default: return '';
|
|
}
|
|
};
|
|
});
|
|
|
|
app.filter('canUpdate', 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('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('prettyEmailAddresses', function () {
|
|
return function prettyEmailAddresses(addresses) {
|
|
if (!addresses) return '';
|
|
if (addresses === '<>') return '<>';
|
|
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
|
|
return addresses.slice(1, -1);
|
|
};
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
// handles various states and triggers a href or configure/repair view
|
|
// used by attaching to controller $scope
|
|
// if $scope.appPostInstallConfirm.show(app); exists it will be called if not yet confirmed
|
|
function onAppClick(app, $event, isOperator, $scope) {
|
|
function stopEvent() {
|
|
$event.originalEvent.stopPropagation();
|
|
$event.originalEvent.preventDefault();
|
|
}
|
|
|
|
if (app.installationState !== ISTATES.INSTALLED) {
|
|
if (app.installationState === ISTATES.ERROR && isOperator) $scope.showAppConfigure(app, 'repair');
|
|
return stopEvent();
|
|
}
|
|
|
|
// app.health can also be null to indicate insufficient data
|
|
if (!app.health) return stopEvent();
|
|
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
|
if (app.runState === RSTATES.STOPPED) return stopEvent();
|
|
|
|
if (app.health === HSTATES.UNHEALTHY || app.health === HSTATES.ERROR || app.health === HSTATES.DEAD) {
|
|
if (isOperator) $scope.showAppConfigure(app, 'repair');
|
|
return stopEvent();
|
|
}
|
|
|
|
if (app.pendingPostInstallConfirmation && $scope.appPostInstallConfirm) {
|
|
$scope.appPostInstallConfirm.show(app);
|
|
return stopEvent();
|
|
}
|
|
}
|
|
|
|
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('<i>hidden</i>');
|
|
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'
|
|
},
|
|
require: '^form',
|
|
link: function ($scope, element, attrs, formCtrl) {
|
|
$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 = $scope.tagArray();
|
|
|
|
// prevent adding empty or existing items
|
|
if ($scope.tagText.length === 0 || tagArray.indexOf($scope.tagText) !== -1) {
|
|
return $scope.tagText = '';
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
formCtrl.$setDirty();
|
|
return $scope.inputTags = tagArray.join(' ');
|
|
};
|
|
$scope.$watch('tagText', function (newVal, oldVal) {
|
|
var tempEl;
|
|
if (!(newVal === oldVal && newVal === undefined)) {
|
|
tempEl = $('<span>' + newVal + '</span>').appendTo('body');
|
|
$scope.inputWidth = tempEl.width() + 5;
|
|
if ($scope.inputWidth < $scope.defaultWidth) {
|
|
$scope.inputWidth = $scope.defaultWidth;
|
|
}
|
|
return tempEl.remove();
|
|
}
|
|
});
|
|
element.bind('click', function () {
|
|
element[0].firstChild.lastChild.focus();
|
|
});
|
|
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) {
|
|
e.preventDefault();
|
|
return $scope.$apply('addTag()');
|
|
}
|
|
});
|
|
},
|
|
template:
|
|
'<div class="tag-input-container">' +
|
|
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
|
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
|
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
|
'</div>' +
|
|
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
|
'</div>'
|
|
};
|
|
});
|
|
|
|
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
|
|
fitTextConfigProvider.config = {
|
|
loadDelay: 250,
|
|
compressor: 0.9,
|
|
min: 8,
|
|
max: 24
|
|
};
|
|
}]);
|
|
|
|
app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
|
|
$scope.initialized = false; // used to animate the UI
|
|
$scope.user = Client.getUserInfo();
|
|
$scope.installedApps = Client.getInstalledApps();
|
|
$scope.config = {};
|
|
$scope.client = Client;
|
|
$scope.subscription = {};
|
|
$scope.notificationCount = 0;
|
|
$scope.hideNavBarActions = $location.path() === '/logs';
|
|
$scope.backgroundImageUrl = '';
|
|
|
|
$scope.reboot = {
|
|
busy: false,
|
|
|
|
show: function () {
|
|
$scope.reboot.busy = false;
|
|
$('#rebootModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.reboot.busy = true;
|
|
|
|
Client.reboot(function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$('#rebootModal').modal('hide');
|
|
|
|
// trigger refetch to show offline banner
|
|
$timeout(function () { Client.getStatus(function () {}); }, 5000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.isActive = function (url) {
|
|
if (!$route.current) return false;
|
|
return $route.current.$$route.originalPath.indexOf(url) === 0;
|
|
};
|
|
|
|
$scope.logout = function (event) {
|
|
event.stopPropagation();
|
|
$scope.initialized = false;
|
|
Client.logout();
|
|
};
|
|
|
|
$scope.openSubscriptionSetup = function () {
|
|
Client.openSubscriptionSetup($scope.subscription);
|
|
};
|
|
|
|
// NOTE: this function is exported and called from the appstore.js
|
|
$scope.updateSubscriptionStatus = function () {
|
|
Client.getSubscription(function (error, subscription) {
|
|
if (error && error.statusCode === 412) return; // not yet registered
|
|
if (error && error.statusCode === 402) return; // invalid appstore token
|
|
if (error) return console.error(error);
|
|
|
|
$scope.subscription = subscription;
|
|
});
|
|
};
|
|
|
|
function refreshNotifications() {
|
|
if (!Client.getUserInfo().isAtLeastAdmin) return;
|
|
|
|
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
|
|
if (error) console.error(error);
|
|
else $scope.notificationCount = results.length;
|
|
});
|
|
}
|
|
|
|
// update state of acknowledged notification
|
|
$scope.notificationAcknowledged = function () {
|
|
refreshNotifications();
|
|
};
|
|
|
|
function redirectOnMandatory2FA() {
|
|
if (Client.getConfig().mandatory2FA) {
|
|
if (Client.getUserInfo().twoFactorAuthenticationEnabled) return; // user already has 2fa
|
|
if (Client.getUserInfo().source && $scope.config.external2FA) return; // 2fa is external
|
|
|
|
$location.path('/profile').search({ setup2fa: true });
|
|
}
|
|
}
|
|
|
|
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
|
|
$scope.$on('$routeChangeStart', function (/* event */) {
|
|
if ($scope.initialized) redirectOnMandatory2FA();
|
|
});
|
|
|
|
var gPlatformStatusNotification = null;
|
|
function trackPlatformStatus() {
|
|
Client.getPlatformStatus(function (error, result) {
|
|
if (error) return console.error('Failed to get platform status.', error);
|
|
|
|
// see box/src/platform.js
|
|
if (result.message === 'Ready') {
|
|
if (gPlatformStatusNotification) {
|
|
gPlatformStatusNotification.kill();
|
|
gPlatformStatusNotification = null;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!gPlatformStatusNotification) {
|
|
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
|
|
|
|
Notification.primary(options).then(function (result) {
|
|
gPlatformStatusNotification = result;
|
|
$timeout(trackPlatformStatus, 5000);
|
|
});
|
|
} else {
|
|
gPlatformStatusNotification.message = result.message;
|
|
$timeout(trackPlatformStatus, 5000);
|
|
}
|
|
});
|
|
}
|
|
|
|
// this loads the very first thing when accessing via IP or domain
|
|
function init() {
|
|
Client.getProvisionStatus(function (error, status) {
|
|
if (error) return Client.initError(error, init);
|
|
|
|
if (redirectIfNeeded(status, 'dashboard')) return; // we got redirected...
|
|
|
|
// check version and force reload if needed
|
|
if (!localStorage.version) {
|
|
localStorage.version = status.version;
|
|
} else if (localStorage.version !== status.version) {
|
|
localStorage.version = status.version;
|
|
window.location.reload(true);
|
|
}
|
|
|
|
console.log('Running dashboard version ', localStorage.version);
|
|
|
|
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
|
|
async.series([
|
|
Client.refreshProfile.bind(Client),
|
|
Client.refreshConfig.bind(Client),
|
|
Client.refreshAvailableLanguages.bind(Client),
|
|
Client.refreshInstalledApps.bind(Client)
|
|
], function (error) {
|
|
if (error) return Client.initError(error, init);
|
|
|
|
// now mark the Client to be ready
|
|
Client.setReady();
|
|
|
|
$scope.config = Client.getConfig();
|
|
|
|
if (Client.getUserInfo().hasBackgroundImage) {
|
|
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
|
|
document.getElementById('mainContentContainer').classList.add('has-background');
|
|
}
|
|
|
|
$scope.initialized = true;
|
|
|
|
redirectOnMandatory2FA();
|
|
|
|
$interval(refreshNotifications, 60 * 1000);
|
|
refreshNotifications();
|
|
|
|
Client.getSubscription(function (error, subscription) {
|
|
if (error && error.statusCode === 412) return; // not yet registered
|
|
if (error && error.statusCode === 402) return; // invalid appstore token
|
|
if (error) return console.error(error);
|
|
|
|
$scope.subscription = subscription;
|
|
|
|
// only track platform status if we are registered
|
|
trackPlatformStatus();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
Client.onConfig(function (config) {
|
|
if (config.cloudronName) {
|
|
document.title = config.cloudronName;
|
|
}
|
|
});
|
|
|
|
init();
|
|
|
|
// setup all the dialog focus handling
|
|
['updateModal'].forEach(function (id) {
|
|
$('#' + id).on('shown.bs.modal', function () {
|
|
$(this).find('[autofocus]:first').focus();
|
|
});
|
|
});
|
|
}]);
|