Files
cloudron-box/src/js/index.js

588 lines
22 KiB
JavaScript
Raw Normal View History

2018-01-22 13:01:38 -08:00
'use strict';
/* global angular:false */
/* global moment:false */
/* global $:false */
/* global ERROR,ISTATES,HSTATES,RSTATES */
2018-01-22 13:01:38 -08:00
// 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', 'ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
2018-01-22 13:01:38 -08:00
app.config(['NotificationProvider', function (NotificationProvider) {
NotificationProvider.setOptions({
delay: 5000,
startTop: 60,
positionX: 'left',
templateUrl: 'notification.html'
});
}]);
app.config(['$translateProvider', function ($translateProvider) {
$translateProvider.useStaticFilesLoader({
prefix: 'translation/',
suffix: '.json'
});
$translateProvider.preferredLanguage('en');
// $translateProvider.preferredLanguage('de');
}]);
2019-07-31 08:08:46 +02:00
// 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',
2019-08-06 10:24:35 +02:00
// 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/**'
2019-07-31 08:08:46 +02:00
]);
});
2018-01-22 13:01:38 -08:00
// 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 %>'
2018-01-22 13:01:38 -08:00
}).when('/appstore', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/appstore/:appId', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/apps', {
controller: 'AppsController',
templateUrl: 'views/apps.html?<%= revision %>'
2019-11-07 11:07:57 +01:00
}).when('/profile', {
controller: 'ProfileController',
templateUrl: 'views/profile.html?<%= revision %>'
2018-06-07 14:22:48 +02:00
}).when('/backups', {
controller: 'BackupsController',
templateUrl: 'views/backups.html?<%= revision %>'
2020-03-18 17:53:50 -07:00
}).when('/branding', {
controller: 'BrandingController',
templateUrl: 'views/branding.html?<%= revision %>'
}).when('/network', {
controller: 'NetworkController',
templateUrl: 'views/network.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/domains', {
controller: 'DomainsController',
templateUrl: 'views/domains.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/email', {
2020-02-11 21:06:34 +01:00
controller: 'EmailsController',
templateUrl: 'views/emails.html?<%= revision %>'
}).when('/email/:domain', {
controller: 'EmailController',
templateUrl: 'views/email.html?<%= revision %>'
}).when('/notifications', {
controller: 'NotificationsController',
templateUrl: 'views/notifications.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).when('/activity', {
controller: 'ActivityController',
templateUrl: 'views/activity.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).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 %>'
2020-10-28 16:14:32 -07:00
}).when('/volumes', {
controller: 'VolumesController',
templateUrl: 'views/volumes.html?<%= revision %>'
2018-01-22 13:01:38 -08:00
}).otherwise({ redirectTo: '/'});
}]);
app.filter('installError', function () {
return function (app) {
if (!app) return false;
2018-01-22 13:01:38 -08:00
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;
};
});
2019-08-29 14:28:45 -07:00
app.filter('activeTask', function () {
return function (app) {
if (!app) return false;
2019-08-29 14:28:45 -07:00
return app.taskId !== null;
};
});
2018-01-22 13:01:38 -08:00
app.filter('installSuccess', function () {
return function (app) {
if (!app) return false;
2018-01-22 13:01:38 -08:00
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);
2019-09-10 13:36:29 -07:00
};
});
app.filter('applicationLink', function() {
return function(app) {
if (!app) return '';
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING && !app.pendingPostInstallConfirmation) {
return 'https://' + app.fqdn;
} else {
return '';
}
};
});
// 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.';
}
2019-09-05 18:15:05 -07:00
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;
2018-01-22 13:01:38 -08:00
};
});
// this appears as tool tip in app grid
2019-09-05 10:05:01 -07:00
app.filter('appProgressMessage', function () {
return function (app) {
2019-09-05 10:05:01 -07:00
var message = app.message || (app.error ? app.error.message : '');
2018-01-22 13:01:38 -08:00
return message;
};
});
2020-09-26 17:50:23 +02:00
// see apps.js $scope.states
app.filter('selectedStateFilter', function () {
return function selectedStateFilter(apps, selectedState) {
return apps.filter(function (app) {
if (!selectedState || !selectedState.state) return true;
if (selectedState.state === 'running') return app.runState === 'running' && app.health === 'healthy' && app.installationState === 'installed';
2020-10-06 13:01:01 -07:00
if (selectedState.state === 'stopped') return app.runState === 'stopped';
2020-09-26 17:50:23 +02:00
2020-10-06 13:01:01 -07:00
return app.runState === 'running' && (app.health !== 'healthy' || app.installationState !== 'installed'); // not responding
2020-09-26 17:50:23 +02:00
});
};
});
app.filter('selectedTagFilter', function () {
return function selectedTagFilter(apps, selectedTags) {
return apps.filter(function (app) {
if (selectedTags.length === 0) return true;
2019-05-17 13:01:23 -07:00
if (!app.tags) return false;
2019-05-17 13:01:23 -07:00
for (var i = 0; i < selectedTags.length; i++) {
if (app.tags.indexOf(selectedTags[i]) === -1) return false;
}
return true;
});
};
});
app.filter('selectedDomainFilter', function () {
2019-05-20 23:40:02 +02:00
return function selectedDomainFilter(apps, selectedDomain) {
return apps.filter(function (app) {
2019-05-20 23:40:02 +02:00
if (!selectedDomain) return true;
if (selectedDomain._alldomains) return true; // magic domain for single select, see apps.js ALL_DOMAINS_DOMAIN
2019-05-20 23:40:02 +02:00
if (selectedDomain.domain === app.domain) return true;
return !!app.alternateDomains.find(function (ad) { return ad.domain === selectedDomain.domain; });
});
};
});
2020-01-06 15:27:31 +01:00
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);
2020-01-06 15:27:31 +01:00
});
};
});
app.filter('prettyDomains', function () {
return function prettyDomains(domains) {
return domains.map(function (d) { return d.domain; }).join(', ');
};
});
2018-01-22 13:01:38 -08:00
app.filter('installationActive', function () {
return function (app) {
2018-01-22 13:01:38 -08:00
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 () {
2020-05-12 21:30:57 -07:00
return function(app) {
if (!app) return '';
var waiting = app.progress === 0 ? ' (Queued)' : '';
2018-01-22 13:01:38 -08:00
switch (app.installationState) {
case ISTATES.PENDING_INSTALL:
return 'Installing' + waiting;
case ISTATES.PENDING_CLONE:
return 'Cloning' + waiting;
2019-09-10 14:30:44 -07:00
case ISTATES.PENDING_LOCATION_CHANGE:
case ISTATES.PENDING_CONFIGURE:
case ISTATES.PENDING_RECREATE_CONTAINER:
case ISTATES.PENDING_DEBUG:
return 'Configuring' + waiting;
case ISTATES.PENDING_RESIZE:
return 'Resizing' + waiting;
2019-09-10 14:30:44 -07:00
case ISTATES.PENDING_DATA_DIR_MIGRATION:
return 'Migrating data' + waiting;
2018-01-22 13:01:38 -08:00
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;
2019-09-22 01:01:47 -07:00
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';
}
2018-01-22 13:01:38 -08:00
case ISTATES.INSTALLED: {
if (app.debugMode) {
2019-12-20 11:53:06 -08:00
return 'Recovery Mode';
} else if (app.runState === 'running') {
2018-01-22 13:01:38 -08:00
if (!app.health) return 'Starting...'; // no data yet
if (app.health === HSTATES.HEALTHY) return 'Running';
return 'Not responding'; // dead/exit/unhealthy
2019-09-22 01:01:47 -07:00
} else if (app.runState === 'stopped') return 'Stopped';
2018-01-22 13:01:38 -08:00
else return app.runState;
}
default: return app.installationState;
}
};
});
2019-09-23 23:28:14 -07:00
app.filter('taskName', function () {
2019-11-23 17:49:13 -08:00
return function(installationState) {
switch (installationState) {
2019-09-23 23:28:14 -07:00
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_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';
2019-11-23 17:49:13 -08:00
default: return installationState || '';
2019-09-23 23:28:14 -07:00
}
};
});
2019-09-23 23:51:15 -07:00
app.filter('errorSuggestion', function () {
return function (error) {
if (!error) return '';
2019-09-23 23:51:15 -07:00
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';
2019-09-23 23:51:15 -07:00
default: return '';
}
};
});
2018-01-22 13:01:38 -08:00
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('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
2020-02-10 13:44:29 -08:00
return function prettyDate(utc) {
var date = new Date(utc), // this converts utc into browser timezone and not cloudron timezone!
2018-01-22 13:01:38 -08:00
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 () {
2020-02-10 13:44:29 -08:00
return function prettyLongDate(utc) {
return moment(utc).format('MMMM Do YYYY, h:mm:ss a'); // this converts utc into browser timezone and not cloudron timezone!
};
});
2019-03-06 15:54:21 +01:00
app.filter('prettyShortDate', function () {
2020-02-10 13:44:29 -08:00
return function prettyShortDate(utc) {
return moment(utc).format('MMMM Do YYYY'); // this converts utc into browser timezone and not cloudron timezone!
2019-03-06 15:54:21 +01:00
};
});
2020-02-12 15:37:05 +01:00
app.filter('prettyEmailAddresses', function () {
return function prettyEmailAddresses(addresses) {
2020-02-12 23:17:24 -08:00
if (!addresses || addresses === '<>') return '<>';
2020-02-12 15:37:05 +01:00
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
return addresses.slice(1, -1);
};
});
2018-01-22 13:01:38 -08:00
// 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 */) {
2018-01-22 13:01:38 -08:00
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('<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) {
2018-01-22 13:01:38 -08:00
$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) {
2018-01-22 13:01:38 -08:00
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 = '';
2018-01-22 13:01:38 -08:00
}
2018-01-22 13:01:38 -08:00
tagArray.push($scope.tagText);
$scope.inputTags = tagArray.join(' ');
2018-01-22 13:01:38 -08:00
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(' ');
2018-01-22 13:01:38 -08:00
};
$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();
});
2018-01-22 13:01:38 -08:00
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) {
2018-01-22 13:01:38 -08:00
e.preventDefault();
return $scope.$apply('addTag()');
}
});
},
template:
'<div class="tag-input-container">' +
2020-03-25 06:48:45 +01:00
'<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)">&times;</button>' +
2018-01-22 13:01:38 -08:00
'</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
};
}]);